> ## Documentation Index
> Fetch the complete documentation index at: https://docs.topograph.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Development Environment

> Build your integration against every country, with generated data and no charges

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.

<Info>
  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.
</Info>

## Creating one

In the product app, go to **Developer → Environments** and click **New
environment**. You get a key immediately:

```
sk_test_kR3nQ8vYpL2mXwF7dTgHjB4c
```

Or over the API, with your **live** key:

```bash theme={null}
curl -X POST https://api.topograph.co/v2/environments \
  -H "Content-Type: application/json" \
  -H "x-api-key: sk_live_YOUR_KEY" \
  -d '{"name": "dev-hugo"}'
```

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.

<Warning>
  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.
</Warning>

## Using one

Swap the key. Nothing else changes: same endpoints, same request bodies, same
response shapes.

```bash theme={null}
curl -X POST https://api.topograph.co/v2/company \
  -H "Content-Type: application/json" \
  -H "x-api-key: sk_test_YOUR_KEY" \
  -d '{"countryCode": "FR", "id": "552100554", "dataPoints": ["company", "legalRepresentatives"]}'
```

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.

<Note>
  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.
</Note>

<Info>
  Development traffic is stored separately from live traffic, so it never
  appears in your live request history, your usage figures or your invoices.
</Info>

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:

```bash theme={null}
curl -X POST https://api.sandbox.topograph.co/v2/sandbox/companies \
  -H "x-api-key: sk_test_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
        "countryCode": "FR",
        "company": {
          "legalName": "Acme Trading SAS",
          "status": { "localName": "Radiée", "active": false }
        }
      }'
```

The response carries the `identifier` to request it by, minted in that
country's own format:

```json theme={null}
{ "countryCode": "FR", "identifier": "418166096", "company": { … } }
```

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.

```bash theme={null}
curl -X PATCH https://api.sandbox.topograph.co/v2/sandbox/companies/418166096 \
  -H "x-api-key: sk_test_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"company": {"legalAddress": {"city": "Lyon"}}}'
```

`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:

```bash theme={null}
curl -X POST https://api.sandbox.topograph.co/v2/sandbox/reset \
  -H "x-api-key: sk_test_YOUR_KEY"
```

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:

```bash theme={null}
# Country has no beneficial-ownership source → same error as live
curl -X POST https://api.topograph.co/v2/company \
  -H "x-api-key: sk_test_YOUR_KEY" \
  -d '{"countryCode": "XX", "id": "123", "dataPoints": ["ultimateBeneficialOwners"]}'
```

That is deliberate. A sandbox that answered everything would let you ship an
integration that breaks the first time it runs for real.

<Warning>
  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.
</Warning>

## Test identifiers

The same magic identifiers the [TEST country](/guides/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:

| Identifier                 | Result                                                                                                                                                                                                       |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `RESOURCE_NOT_FOUND`       | `resource_not_found` (404)                                                                                                                                                                                   |
| `INVALID_REQUEST`          | `invalid_request` (400)                                                                                                                                                                                      |
| `PROCESSING_FAILED`        | `processing_failed` (422)                                                                                                                                                                                    |
| `SERVICE_UNAVAILABLE`      | `service_unavailable` (503)                                                                                                                                                                                  |
| `SOURCE_UNAVAILABLE`       | The register behind the datapoint is down                                                                                                                                                                    |
| `INSUFFICIENT_FUNDS`       | The refusal a depleted wallet produces                                                                                                                                                                       |
| `ONBOARDING_TIMEOUT`       | Fast source missed the onboarding deadline                                                                                                                                                                   |
| `NO_DATA_AVAILABLE`        | Sources answered but held no data                                                                                                                                                                            |
| `NO_DOCUMENT_AVAILABLE`    | The register has no such document on file                                                                                                                                                                    |
| `DATAPOINT_NOT_APPLICABLE` | Datapoint does not exist for this legal form                                                                                                                                                                 |
| `BUDGET_EXCEEDED`          | The request hit its configured budget ceiling                                                                                                                                                                |
| `REQUEST_CANCELLED`        | The request was cancelled mid-flight                                                                                                                                                                         |
| `PROCESSING_INTERRUPTED`   | Processing was interrupted before completion                                                                                                                                                                 |
| `PARTIAL_FAIL`             | Data delivered even though one source failed. The public status stays `succeeded` (source-level imperfection is never surfaced), so this checks your handler treats a delivered response the same either way |

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

| Identifier  | Delivered after |
| ----------- | --------------- |
| `DELAY_1M`  | 1 minute        |
| `DELAY_10M` | 10 minutes      |
| `DELAY_1H`  | 1 hour          |
| `DELAY_10H` | 10 hours        |

### Company status

Generated companies are active. These pin the branch a KYB flow exists to
handle, the decline:

| Identifier  | Company returned                                          |
| ----------- | --------------------------------------------------------- |
| `DISSOLVED` | `status.active: false`, standardized status `CLOSED`      |
| `INACTIVE`  | On the register but not operating (`INACTIVE_NOT_CLOSED`) |

### 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:

| Identifier     | What comes back                                                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `SPARSE`       | Valid but minimal: every optional field absent (no address, legal form, capital, activities). The thin record your code must not assume away |
| `UNICODE`      | Accented, Cyrillic, CJK and RTL names, people and cities, for encoding, collation, and column rendering                                      |
| `LONG_NAMES`   | A 300-character legal name, past what most UI columns and a few `varchar(255)` schemas survive                                               |
| `MANY_RESULTS` | Search fills the whole page, for pagination and "too many matches, refine your query"                                                        |

### 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:

| Query           | Result set                                                |
| --------------- | --------------------------------------------------------- |
| `MATCH_ID`      | First result reports an `id` (1:1) match                  |
| `MATCH_EXACT`   | First result reports `exactLegalName`                     |
| `MATCH_PARTIAL` | First result reports `partialId`                          |
| `MATCH_DEFAULT` | First result reports `default`                            |
| `SINGLE_RESULT` | Exactly one result, the auto-select branch                |
| `NO_RESULTS`    | Empty set, the "nothing found, offer manual entry" branch |

### Ownership structures

| Identifier prefix | Structure                                             |
| ----------------- | ----------------------------------------------------- |
| `GRAPH_SIMPLE`    | Individual shareholders                               |
| `GRAPH_CHAIN`     | A corporate shareholder, with an individual behind it |
| `GRAPH_UBO`       | A single beneficial owner with majority control       |
| `GRAPH_NONE`      | No shareholders and no beneficial owners on file      |

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.

<Note>
  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.
</Note>

## 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:

```bash theme={null}
# realistic, jittered timings
curl -X POST https://api.sandbox.topograph.co/v2/environments/ci/settings \
  -H "x-api-key: sk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"simulateLatency": true}'

# or an exact 2.5s for every answer
curl -X POST https://api.sandbox.topograph.co/v2/environments/ci/settings \
  -H "x-api-key: sk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"fixedLatencyMs": 2500}'

# back to instant
curl -X POST https://api.sandbox.topograph.co/v2/environments/ci/settings \
  -H "x-api-key: sk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"fixedLatencyMs": null, "simulateLatency": false}'
```

## 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:

```bash theme={null}
curl -X POST https://api.sandbox.topograph.co/v2/sandbox/monitor-webhook \
  -H "x-api-key: sk_test_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"monitorId": "YOUR_MONITOR_ID", "changeCategories": ["status", "address"]}'
```

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.

| Body                                                                                                  | Event delivered                              |
| ----------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `{"monitorId": "…"}`                                                                                  | `monitor.notification`, category `other`     |
| `{"monitorId": "…", "changeCategories": ["status","ownership"]}`                                      | `monitor.notification` with those categories |
| `{"monitorId": "…", "changeCategories": ["disappeared"], "monitorHasBeenDeactivated": true}`          | the company-disappeared case                 |
| `{"monitorId": "…", "event": "monitor.datapoint_blocked", "datapoint": "ultimateBeneficialOwners"}`   | credential gate closes                       |
| `{"monitorId": "…", "event": "monitor.datapoint_unblocked", "datapoint": "ultimateBeneficialOwners"}` | credential gate reopens                      |

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:

```bash theme={null}
curl -X PUT https://api.sandbox.topograph.co/v2/sandbox/wallet \
  -H "x-api-key: sk_test_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"balanceInCreditCents": 120, "autoRefill": false}'
```

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.

```bash theme={null}
# create
curl -X POST https://api.topograph.co/v2/environments \
  -H "x-api-key: sk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"name": "ci"}'

# list (keys included)
curl https://api.topograph.co/v2/environments -H "x-api-key: sk_live_YOUR_KEY"

# discard
curl -X DELETE https://api.topograph.co/v2/environments/ci \
  -H "x-api-key: sk_live_YOUR_KEY"
```

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.

<Note>
  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.
</Note>
