sandbox.topograph.co for the app, api.sandbox.topograph.co
for the API) with their own database. Nothing you do in a sandbox can touch a
live record, a real register, or a real invoice, structurally rather than by
configuration. And you do not need a production account to have one: sign up
on the sandbox, create an organisation, and you have a working environment.
Use it to build and test. Switch to your live key when you want real data.
Development environments keep working when your trial ends, when your
subscription lapses, and when your balance is empty. They cost nothing to
run, so we never close them.
Creating one
In the product app, go to Developer → Environments and click New environment. You get a key immediately:Using one
Swap the key. Nothing else changes: same endpoints, same request bodies, same response shapes.sk_test_ key can only ever reach generated
data, and a sk_live_ key can only ever reach real data.
What you get back
Data is generated from the country’s own definitions, so it has the right shape for the country you asked for:- Identifiers in the country’s real format, under the country’s real keys, a French company carries a SIREN, a German one a Handelsregister number.
- Legal forms drawn from the values that country’s register actually publishes. Never a retired one.
- Roles from the country’s own role dictionary.
- Activity codes in the systems that country uses.
- Documents exactly as the country declares them, with their real product codes.
Every generated field is marked with the source
generated and the register
topograph_development_environment. If you ever wonder whether a payload
came from a sandbox, that is the tell.Development traffic is stored separately from live traffic, so it never
appears in your live request history, your usage figures or your invoices.
Companies you define
Generated data covers every country and every identifier, which is what makes the sandbox broad. It is the wrong tool when you need a specific company: to reproduce a bug a customer reported, to build a demo or a screenshot, or to assert on your own matching logic. So define one:identifier to request it by, minted in that
country’s own format:
/v2/company with that identifier returns your company, and
/v2/search finds it by name or identifier alongside generated results.
Send only what you care about. A fixture is merged over generated data, so
pinning one field costs one field: everything you leave out (address, legal
form, incorporation date, activities) is still generated, and the record stays
complete and country-correct.
Making a company change
PATCH it, and the next request returns the new version. That is what a
monitoring integration needs: a company that genuinely changes between two
fetches, rather than a synthetic event.
GET /v2/sandbox/companies lists what you have defined;
DELETE /v2/sandbox/companies/{countryCode}/{identifier} gives the identifier
back to generated data. If you pin an identifier yourself, it is stored in the
country’s canonical form and validated exactly as /v2/company validates it,
so a fixture can never sit under an identifier the API would refuse.
Starting from a clean slate
A CI run wants a known state.POST /v2/sandbox/reset clears this
environment’s request history, search logs and webhook logs, and restores the
virtual wallet:
{"keepWallet": true} when a suite deliberately set a low balance to test
depletion and only wants the history cleared. A reset is refused with 409
while a request is still in progress (a DELAY_1H sleep, say): let it finish
or cancel it first.
Coverage is honest
A development environment will not invent data a country cannot deliver. Ask for a datapoint a country does not support and you get the same error you would get in production:Test identifiers
The same magic identifiers the TEST country documents work in every country here, so you can exercise your error handling against country-correct data.Errors
Every error code the live API can return has an identifier: the code in upper case. Whatever branch your error handling has, you can reach it:
Messages, HTTP statuses and retryability come from the same registry the live
API uses, so what you handle here is exactly what you will handle there.
RATE_LIMITED is the one exception, and it is worth knowing why: rate limiting
happens before a request reaches the data pipeline, so it is not a data error
at all. Use it as the identifier (or the search query) with your sk_test_ key
and you get a genuine 429 with Retry-After and the X-RateLimit-* headers,
produced by the same guard that throttles real traffic, which is what your
retry and backoff code needs to see.
Delivery delays
Company status
Generated companies are active. These pin the branch a KYB flow exists to handle, the decline:Data shapes that break integrations
Generated companies are complete, mid-length and Latin-alphabet. Real registers are not, and that gap is where integrations fail in production. These pin the shapes worth testing against:Search results and match reasons
A sandbox search returns a realistic result set, not a single row: the query itself first, then plausible neighbours carrying identifiers in that country’s own format. Every row resolves: follow any of them into/v2/company and you get the company the row promised.
Match reasons are inferred the way live infers them: an identifier resolves
id (1:1, with the matched identifier attached), a name matches
exactLegalName, near-misses come back as partialId, the rest default;
and results arrive ranked id > exactLegalName > partialId > default.
To drive one branch of your resolution logic directly, pin it:
Ownership structures
All of these accept a suffix, so you can keep several in-flight requests apart
in your own logs:
RESOURCE_NOT_FOUND-042, DELAY_1H_A, GRAPH_CHAIN_007.
They also compose where it makes sense: RESOURCE_NOT_FOUND_DELAY_1M fails
after a minute, which is the case your timeout-plus-error handling needs.
Test identifiers are deliberately not any country’s identifier format, so a
development environment accepts them where live would reject the shape. An
ordinary malformed identifier is still rejected in the sandbox exactly as in
production, so you cannot ship code that sends ids production refuses.
Response times
By default a development environment answers instantly, which is what a CI suite wants. Use aDELAY_* identifier when you need to exercise polling,
timeouts or a loading state.
You can also switch on Simulate real response times for an environment.
Each datapoint then waits around that country’s published latency for it,
with a little natural variation from request to request, and with different
datapoints landing at different moments, exactly as a real request streams
in. If your integration blocks on the whole response instead of consuming the
stream, this is the setting that will show you.
For tests that assert on timing, set a fixed response time instead: every
answer then takes exactly that many milliseconds, no jitter. The DELAY_*
identifiers keep working in every mode.
Both are settable in the app, and over the sandbox API with your default
sandbox key, so a CI job can configure the environment it is about to use:
Webhooks
Each environment has its own webhook application, its own endpoints and its own signing secret, configured independently of live. Point development deliveries at a tunnel on your laptop and your live endpoint never sees them; the two streams cannot interleave, because they are different applications. To configure one: switch to the environment in the app, open Webhooks, and the portal you see belongs to that environment (the page names which one you are editing). Add your endpoint and copy its signing secret from there. Two extra affordances for development deliveries:- Every development payload carries
"environment": "development"at the top level, so a handler receiving both streams at one URL can tell generated data from real without comparing signing secrets. - The
DELAY_*identifiers exercise the full async path: create a request withDELAY_1Mand your endpoint receives the completion webhook a real minute later, exactly as a slow register would deliver it.
Monitoring events on demand
Monitoring is the one part of the product you cannot rehearse while building: a realmonitor.notification arrives when a register changes, on the daily
check’s schedule, days of waiting for an event that may never come. A
development environment lets you deliver the genuine event now.
Create the monitor first, exactly as you would in production
(POST /v2/monitors), then trigger an event for it:
The monitor itself is never modified: no change is recorded, nothing is
deactivated, and the next scheduled check is unaffected. Live accounts get a
403 here: a production notification means a register really changed.
Everything else that is configured per account works the same way: workspaces,
API key rotation, request history. A development environment has its own.
Billing that behaves like the real thing
A development environment has its own wallet, funded with virtual credits. Every request bills the real catalog price against it, so the balance depletes exactly as a live wallet would: you see genuine prices on every event, and your integration exercises real billing behaviour. The difference is that the credits cost nothing and the wallet refills itself: whenever a request would leave it below 10,000 credits, it tops back up to 20,000 automatically. It can never run dry, never blocks a test suite, and never touches a card. To test what happens when a wallet DOES run dry, take control of it:insufficient_funds, the same
refusal, from the same billing path, your live integration would see. Set any
balance and "autoRefill": true to go back to normal. (GET on the same
route reads the current state; the INSUFFICIENT_FUNDS identifier is the
quicker option when you only need the error branch.)
Usage → Development environments shows what each sandbox has spent. Because
the prices are real, that figure is also your live-bill estimate: run the
integration end to end here and read off what production will cost.
Creating and discarding programmatically
Environments are managed from Developer → Environments in the sandbox app, or over the sandbox API with the key of your default sandbox (the environment your main organisation gets automatically; its key is on the same page). Only an organisation admin can create, rotate or delete one.401. Its request history stays readable, and the name becomes
available again for a new environment.
Managing environments keeps working when your trial ends or a subscription
lapses, and so does every environment you already have. A sandbox costs
nothing to run, so losing one mid-integration would make no sense. Live data
access is unaffected by this: a lapsed
sk_live_ key still cannot fetch real
register data.