The sandbox

The real API, with every number fabricated, for any verified account: no plan, no card, no add-on. It is available.

The sandbox in one paragraph

Every number in the sandbox is fabricated. The sandbox exists so you can build and test an integration against the real API — real endpoints, real parameters, real response shapes, the real industry and occupation code hierarchies — without a subscription and without a card. What it does not give you is data. Nothing you read from a sandbox key is a measurement of anything, and no sandbox value may be published, cited, charted for anyone else, or presented as data. If a figure reached your users from a sandbox key, that is a mistake to correct, not a number to caveat.

Every sandbox response tells you which it is. Look for X-DTG-Sandbox: true in the headers, "sandbox": true in meta, and a # sandbox: true line in the preamble of any CSV you export.

Getting a key

  1. Sign up and verify your email address. That is the whole requirement — no plan, no card, no add-on, no administrator.
  2. Mint a key from your account’s API keys page, or with the app’s session:

Request

curl -X POST "https://api.datatactical.group/v2/account/keys" \
  --cookie "$YOUR_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"name":"my integration","mode":"sandbox"}'

Response · replayed 2026-10-05 against vintage 20260913. Trimmed: the key after its prefix (it is shown once, to you).

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
​
{ "id": "7b79f5ad-c35b-4387-919e-84e6430aebb9", "name": "my integration",
    "mode": "sandbox", "key_prefix": "dtg_sandbox_jx2j",
    "created_at": "2026-10-05T04:26:55.320Z", "key": "dtg_sandbox_…",
    "sandbox": true,
    "note": "Shown once; store it now. This key resolves SYNTHETIC data only — every value behind it is fabricated and must never be published as data." }

The key looks like dtg_sandbox_… and is shown once, in the response that creates it. You may hold two active sandbox keys, counted separately from your live keys — spending one here never costs you a live slot, so you can still rotate a production credential without downtime.

Sandbox keys work only against the v2 API. They are refused on the legacy /v1-compat surface, and a sandbox key can never read a real vintage. A sandbox key reads, and may create and edit clusters; any other change is refused with a 403 and the code sandbox_read_only.

Your first request

The sandbox answers at https://api.datatactical.group/sandbox, with the same paths as live after it:

Request

curl "https://api.datatactical.group/sandbox/v2/industry/series?areas=00&naics=10&from=2020&to=2025" \
  -H "Authorization: Bearer $DTG_SANDBOX_KEY"

Response · replayed 2026-10-05 in the sandbox, vintage sandbox-2026.2. Every number in it is fabricated. Trimmed: data.points to 2 of 6.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-DTG-Sandbox: true
​
{
  "data": [
    {
      "naics": "10",
      "area": "00",
      "ownership": "all",
      "points": [
        { "period": "2020", "emp": 139131111.43, "estabs": 10436078.01,
            "earnings": 8822844848083.58, "avg_pay": 63413.89,
            "disclosure": 0, "reliability": 1, "naics_restated": true,
            "fips_restated": true, "quarters_present": 4 },
        { "period": "2021", "emp": 143840099.47, "estabs": 10856887.8,
            "earnings": 9629652211500.84, "avg_pay": 66946.92,
            "disclosure": 0, "reliability": 1, "naics_restated": true,
            "fips_restated": true, "quarters_present": 4 }
      ]
    }
  ],
  "meta": {
    "vintage": "sandbox-2026.2",
    "sandbox": true,
    "generated_at": "2026-10-05T04:26:55Z",
    "granularity": "annual",
    "provenance_boundaries": {
      "actuals": { "from": "2004q1", "to": "2025q4" },
      "ces": { "from": "2026q1", "to": "2026q2" },
      "projections": { "from": "2026q3", "to": "2036q4" }
    },
    "geography": { "requested": 1, "returned": 1, "excluded": 0 },
    "pagination": { "cursor": null, "next_cursor": null, "count": 1 }
  }
}

Everything documented in the API reference works: /series, /map, /lq, both domains, every measure, the taxonomy endpoints, area clusters, the vintage selector, and CSV export on every data endpoint.

What is real and what is not

This matters more here than it looks, because the parts that ARE real are what make the sandbox worth testing against — and the parts that are not will make a test that asserts on values fail the moment you switch to live.

Real Synthetic
Every endpoint, parameter, problem code and response shape Every numeric value, without exception
The whole NAICS and SOC hierarchy — real codes, real titles, every level —
The nation and every state + DC, with their real FIPS and real names —
The full period axis, 2004 through the projection horizon —
All four ownerships, every fact field the live API serves, the bands (occupation wage percentiles are stored but not served, in the sandbox exactly as in live — D11, D19) —
disclosure codes, the sparse-zero pattern, provenance_boundaries —
— Counties: invented names, invented FIPS codes, invented MSAs

The five sandbox counties

There are no real counties in the sandbox. There are five invented ones, each sitting inside a real state and each shaped after a real county chosen for a particular statistical character, so the awkward cases are testable:

FIPS Name What it is for
22801 Anselm Parish, LA A mid-size market with mixed suppression
48801 Halcyon County, TX Dense, large values, almost nothing suppressed
30801 Cinder County, MT Small, suppression-dominated — lots of disclosure = 1
48803 Redgate County, TX With Westmark, a complete cross-state metro
05801 Westmark County, AR The other half of that metro

Redgate and Westmark together are MSA 99001, “Redgate-Westmark, TX-AR” — an invented metro that spans two states, so msa:99001 exercises the cross-state case properly. MSA 99002 covers Halcyon alone.

Two consequences to design around:

  • areas=state:XX only works for AR, LA, MT and TX. Every other state has no counties in the sandbox file, and the request is a 400. State and national figures work for all 51.
  • There are no “Unknown Or Undefined” catch-all areas. The live county universe has one per state; the sandbox has none.

Numbers do not add up across geography

A sandbox county does not sum to its state, and the states do not sum to the nation. Each area was perturbed on its own. (This is also true of the real data in the sense that matters — the live vintage is not built by summing counties either — but in the sandbox the gaps are arbitrary rather than meaningful.)

What does hold exactly, to the cent, is the code hierarchy: within one area, ownership and period, a parent industry or occupation equals the sum of its children. So a test that sums a non-overlapping set of NAICS codes and compares it against their parent will pass here and pass on live.

The two vintages

/v2/vintages lists two, and both are entitled:

id label
sandbox-2026.2 SANDBOX 2026.2 — the default
sandbox-2026.1 SANDBOX 2026.1

They hold the same structure and different numbers, so you can test vintage switching for real: point ?vintage=sandbox-2026.1 at a request and the values move, exactly as they would across a live release.

Restatement flags. SANDBOX 2026.2 restates the nation’s total-industry private-ownership series: every period of it carries naics_restated: true and fips_restated: true. In SANDBOX 2026.1 the same series carries the flags the real data does: fips_restated on no period, and naics_restated only on the periods before 2017. Fetch it in both and you have a restatement test:

Request

curl "https://api.datatactical.group/sandbox/v2/industry/series?areas=00&naics=10&ownership=private&granularity=quarterly" \
  -H "Authorization: Bearer $DTG_SANDBOX_KEY"

Response · replayed 2026-10-05 in the sandbox, vintage sandbox-2026.2. Every number in it is fabricated. Trimmed: data.points to 1 of 132.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-DTG-Sandbox: true
​
{
  "data": [
    {
      "naics": "10",
      "area": "00",
      "ownership": "private",
      "points": [
        { "period": "2004q1", "emp": 104919627.61, "estabs": 7869210.86,
            "earnings": 1028775629254.63, "avg_pay": 9805.37,
            "disclosure": 0, "reliability": 1, "naics_restated": true,
            "fips_restated": true }
      ]
    }
  ],
  "meta": {
    "vintage": "sandbox-2026.2",
    "sandbox": true,
    "generated_at": "2026-10-05T04:26:55Z",
    "granularity": "quarterly",
    "provenance_boundaries": {
      "actuals": { "from": "2004q1", "to": "2025q4" },
      "ces": { "from": "2026q1", "to": "2026q2" },
      "projections": { "from": "2026q3", "to": "2036q4" }
    },
    "geography": { "requested": 1, "returned": 1, "excluded": 0 },
    "pagination": { "cursor": null, "next_cursor": null, "count": 1 }
  }
}

Response · replayed 2026-10-05 in the sandbox, vintage sandbox-2026.1. Every number in it is fabricated. Trimmed: data.points to 1 of 132.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-DTG-Sandbox: true
​
{
  "data": [
    {
      "naics": "10",
      "area": "00",
      "ownership": "private",
      "points": [
        { "period": "2004q1", "emp": 105761902.24, "estabs": 8056885.96,
            "earnings": 1028736713999.24, "avg_pay": 9726.91,
            "disclosure": 0, "reliability": 1, "naics_restated": true,
            "fips_restated": false }
      ]
    }
  ],
  "meta": {
    "vintage": "sandbox-2026.1",
    "sandbox": true,
    "generated_at": "2026-10-05T04:26:55Z",
    "granularity": "quarterly",
    "provenance_boundaries": {
      "actuals": { "from": "2004q1", "to": "2025q4" },
      "ces": { "from": "2026q1", "to": "2026q2" },
      "projections": { "from": "2026q3", "to": "2036q4" }
    },
    "geography": { "requested": 1, "returned": 1, "excluded": 0 },
    "pagination": { "cursor": null, "next_cursor": null, "count": 1 }
  }
}

Limits

Sandbox
Requests per minute, per key 60
Requests per minute, per organization 120
Points per month 5,000,000
Active sandbox keys per member 2

The monthly points allowance is deliberately generous: points meter data extraction, and there is nothing here to extract. The per-minute ceilings are the opposite case — they are lower than the live ones, because sandbox capacity is shared and free. The sandbox also caps how many queries it runs at once; over that you get a 429 with Retry-After and "code": "sandbox_busy". Retry, and prefer a few large requests to many small ones.

Moving to live

Four things change and one thing does not.

Change your key. A live key is dtg_live_…, requires an annual plan with the API add-on, and resolves real vintages under real vintage ids (20260824, not sandbox-2026.2). If your code hard-codes a sandbox vintage id it will get a 400 on live, so read the id from /v2/vintages or omit the parameter and take the current one. Note the shape differs too: a real vintage id is eight digits, so a validator written as ^\d{8}$ against the live ids will reject every sandbox id, and one written loosely enough to accept sandbox-2026.2 will accept things live never returns.

Change your base URL. The sandbox answers at https://api.datatactical.group/sandbox and the live API at https://api.datatactical.group. A live key sent to the sandbox address is refused with a 403 and the code wrong_instance, and so is a sandbox key sent to the live one; the refusal’s base_url names the address the key belongs at. Keep the two bases in configuration, not in code.

Change your geography. The sandbox FIPS codes do not exist in the live data. Whatever you built against 22801 needs a real county.

Expect your plan’s limits to appear. This is the one that surprises people, because nothing in the sandbox hints at it. The sandbox does not reflect your entitlements. Every sandbox key gets the same full profile regardless of what the organization holding it has bought: API access, CSV, quarterly, prior vintages, full code depth, and every area in the file with no geography gate and no county limit. On live, your plan decides all of it.

The sharpest edge is API access itself. A sandbox key works for an organization that has no API entitlement at all — no annual plan, or a paid plan without the API add-on. The same organization’s live key does not: every v2 data request returns 403 with "code": "plan_required" and "entitlement": "api_access". County capacity is the other one: the sandbox is unmetered, and on live you get the number of counties your plan includes.

So do not read your sandbox behavior as a preview of your live behavior. To see what your organization is actually entitled to, call /v2/account with your live key — called with a sandbox key it reports the sandbox profile, not your plan.

Nothing else changes. Same paths, same parameters, same envelope, same problem codes, same CSV shape — an integration written against the sandbox parses live responses unchanged. (The one path the sandbox does not serve is the legacy /v1-compat surface, noted above; moving to live only adds it.)

Minting a sandbox key does not affect your plan or your billing: you can try the API in the sandbox for as long as you like before you buy.

What the sandbox does and does not track

The sandbox is a fixed snapshot. It is not rebuilt when a new real vintage is released, so its period axis and its industry, occupation and geography code lists stay as they were when it was built, while the live data moves on.

What that means for you:

  • Endpoint shapes, parameters, errors and pagination are the same as live. That is what the sandbox is for, and it does not go stale.
  • Values, periods and code lists are not live and will diverge. Do not hardcode a period boundary, a latest quarter, or a code list read from the sandbox. Read them from the response meta at run time, as you should against live data anyway — the live axis moves every release regardless.
  • Never treat a vintage id as permanent, sandbox or live. Resolve the current vintage from the API rather than pinning one.

An integration written this way needs no change when a real vintage lands.

In short

Build against it freely. Never publish a number from it.