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
- Sign up and verify your email address. That is the whole requirement — no plan, no card, no add-on, no administrator.
- 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:XXonly 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
metaat 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.