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

# Environments

> Live and test: same schema, never the same rows, and the caller never picks.

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.

```bash theme={null}
curl -s $API/v1/organization/environments \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…"
```

```json theme={null}
{
  "environments": [
    { "id": "…", "kind": "live", "auth_mode": "hosted", "created_at": "…" },
    { "id": "…", "kind": "test", "auth_mode": "hosted", "created_at": "…" }
  ]
}
```

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

```
uk_sk_live_…  →  live      uk_pk_live_…  →  live
uk_sk_test_…  →  test      uk_pk_test_…  →  test
```

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.

<Note>
  `GET /v1/me` is the cheapest way to confirm which environment a key opens. Every
  integration should call it once at start-up.
</Note>

## The panel is the exception

Staff are authorized for the whole organization, so panel reads take the
environment as an explicit **view** parameter:

```bash theme={null}
curl -s "$API/v1/organization/contacts?environment=test" \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…"
```

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.

## Auth mode is per environment

Each environment carries its own `auth_mode`, `hosted` or `federated`. That is
deliberate: you can evaluate hosted in test while production stays federated.

```bash theme={null}
curl -s -X PATCH $API/v1/organization/environments/{id} \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…" \
  -H 'Content-Type: application/json' \
  -d '{ "auth_mode": "federated" }'
```

Requires `organization:update`. The endpoints of the other mode refuse rather than
quietly become a second sign-in:

| Called against                                         | Answer                          |
| ------------------------------------------------------ | ------------------------------- |
| `/v1/boot` with `external_id`, on a hosted environment | `409 environment_not_federated` |
| `/v1/contact-auth/signup` on a federated environment   | `409 environment_not_hosted`    |

See [Choosing an auth mode](/en/customer-auth/modes).

## The identity secret

A federated environment also 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.

```bash theme={null}
curl -s $API/v1/organization/environments/{id}/identity \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…"
```

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.

```bash theme={null}
curl -s -X POST $API/v1/organization/environments/{id}/identity/rotate \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…"
```

Both endpoints answer `501 federated_identity_unavailable` when the server cannot
keep the secret encrypted safely.
