Skip to main content
Every organization owns exactly two environments, live and test, seeded when the organization is created. They cannot be created or deleted, and there is never a third. Customer-plane tables carry environment_id alongside organization_id. Test data and live data are the same schema and never the same rows. The difference between the two is what the rows mean, not what they can do. Test is not a reduced copy: the same endpoints answer, the same webhooks fire, the same emails send, the same limits apply. What separates them is that one holds your real customers and the other holds whatever you needed while building, and that nothing you do in test can be seen from live.

The credential decides

This is the rule the whole design turns on. On the machine surface, the environment is resolved from the stored key row on every request. Not from a header, not from a query parameter, not from a body field. There is no way to name one.
Which means:
  • a live id fetched with a test key answers 404 — and the 404 does not reveal that the row exists elsewhere;
  • a contact identified with a test key is unreachable from live;
  • pointing staging at production is not a mistake you can make in a config file, because there is no field to get wrong.
The environment kind is minted into the key’s prefix, so a key that leaks says on its face which mode it opens.
GET /v1/me is the cheapest way to confirm which environment a key opens. Every integration should call it once at start-up.

The panel is the exception

Staff are authorized for the whole organization, so panel reads take the environment as an explicit view parameter:
It defaults to live. Anything other than live or test answers 400 invalid_request. This is the opposite posture from the machine surface, and safe for exactly that reason: the caller is already authorized for both environments, so letting them choose adds no reach. A machine credential gets no such parameter, ever. In the panel UI the active environment is a client-side view preference, marked with the header switch and an amber banner whenever test mode is on.

The identity secret

Every environment holds an identity secret — the HMAC key your server signs external_id values with. It is per environment, minted on first read, and reading it requires api_keys:write: this secret mints verified sessions, so reading it is holding it.
Rotating replaces it and keeps the previous one verifying for a 24-hour grace window — long enough for a fleet to redeploy, short enough that a stolen old secret has a deadline.
Both endpoints answer 501 federated_identity_unavailable when the server cannot keep the secret encrypted safely.

What the two environments share

Almost nothing. Contacts, customers, conversations, events, subscriptions and messages are per environment, and nothing you do in test is visible from live. The exception is your help center. Articles belong to the organization, not to an environment: there is one of them, published once, and both environments read it. An article is something you wrote, reviewed and published, and it says the same thing whichever environment asks. Which matters for the AI auto-responder, because it answers from two kinds of material: A resolved conversation is a transcript of whatever was typed while somebody was building: a rehearsal, a widget tested with nonsense, a refund promised to a contact that does not exist. That is knowledge about your test environment, not about your product, so it never shapes an answer sent to a live customer — and live’s transcripts never shape a test answer either.
A test environment with no resolved conversations of its own answers from your published articles alone. With no published articles either, the auto-responder records no_knowledge, escalates the thread and leaves it in the inbox for a person. That is the correct answer, not a misconfiguration: live’s transcripts are live’s.
The cached answers follow the same line. An answer is cached per environment and retired whenever the material behind it moves — publishing an article, or resolving another conversation in that environment.