Skip to main content
A development environment is a sandbox alongside your live account. It answers for every country Topograph supports, returns generated data instead of register data, and never costs real money. It is a full environment, not a flag: its own API key, its own webhook endpoint, its own workspaces, its own request history, and its own membership. Each environment is a separate organisation with its own member list and roles, so you can give a contractor admin access to a sandbox without giving them anything on live. When an environment is created, everyone already in your organisation is added to it with their current role; from there the two lists are managed independently. It is also physically isolated: development environments run on their own deployment (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:
Or over the API, with your live key:
Create as many as you need: a shared sandbox, a staging mirror, one per developer, or one per CI run created and discarded around the job. They cost nothing to run, so we do not cap them.
Environment management requires your live key. A sk_test_ key can use its own environment but cannot list, create or delete environments, so a sandbox key that leaks cannot expose your live one.

Using one

Swap the key. Nothing else changes: same endpoints, same request bodies, same response shapes.
The key alone decides which environment serves the request. There is no header to set and no flag to forget: a 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.
It is also deterministic: the same country and identifier always return the same company, byte for byte. You can assert on the values in your test suite and they will still hold next month.
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.
Ordered documents come back as real PDFs, laid out like a register extract and filled with the same generated company the datapoints return for that identifier: name, people and address all agree. Every page says it is a sandbox artefact.

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:
The response carries the identifier to request it by, minted in that country’s own format:
From then on /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:
The environment itself is untouched: same API key, same webhook endpoints, same settings, so a job can reset between runs without re-provisioning anything. Pass {"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:
That is deliberate. A sandbox that answered everything would let you ship an integration that breaks the first time it runs for real.
The ownership graph (graph) is not served in a development environment. Traversal fans out across real registers, so rather than half-serve it we decline it. Everything else (search, all company datapoints, documents, webhooks) is available.

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 a DELAY_* 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 with DELAY_1M and 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 real monitor.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 response is the exact payload delivered to your endpoint. It carries your own monitor id and the metadata you set at monitor creation, built by the same code the daily monitoring workflow uses, indistinguishable from a real notification. 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:
With the auto-refill off, requests deplete the balance for real and the one that no longer fits fails with a genuine 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.
Deleting an environment kills its key immediately: the next request with it returns 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.