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

# API key management end to end: issue your first production key and rotate it safely

> One walkthrough that takes a key from minting to retirement — mint scoped sandbox and production keys, attach an IP allowlist, usage limits, and a spend budget, then rotate with zero downtime using the dual-valid grace window.

# API key management end to end

The Developer surface in Devotel Orbit gathers four control families onto one page — the keys themselves, per-key IP allowlists, per-key usage limits, and per-key spend budgets. Each of those families has its own deep-dive guide, linked below, and together they cover every dial individually. What operators still assemble by hand, spread across those guides side-by-side, is the order of operations for one complete key lifecycle: mint the right key, add every control to it, and retire it without breaking a running integration.

This guide is that one-shot recipe. It assumes nothing beyond an account; by the end you have a production key that is scoped, IP-locked, usage-bounded, and budget-aware — and you know exactly how to rotate it when the time comes.

## Why an umbrella guide

Five sibling guides each own one dial, and the recipe only works when all the dials are set in sequence:

* [Choose and scope API keys](/guides/api-keys-messaging-ccaas-cdp) — which scopes a key carries.
* [Prefix-scoped sandbox tokens](/guides/sandbox-prefix-scoped-tokens) — why the first exercise belongs on a `dv_test_` key.
* [Per-API-key IP allowlist](/guides/api-key-ip-allowlist) — bind the key to your source IPs.
* [Per-API-key usage limits](/guides/api-key-usage-limits) and [usage budgets and threshold alerts](/guides/api-key-usage-budget-alerts) — hard request caps and the browser-side badge.
* [Per-API-key spend budgets](/guides/per-api-key-spend-budgets) — a money ceiling per key, alerted once per threshold.

Run them together once, here, and then bookmark whichever one you need to change later.

## Step 1 — Mint a scoped key (sandbox first, then production)

The first key you mint should be a sandbox key, not a production one. A sandbox key (`mode: "test"`, prefix `dv_test_sk_`) can never deliver real traffic or bill your wallet, no matter which endpoint you aim it at — so your integration's first run is free of side effects. Build the whole request shape against sandbox, then mint the production key with the same scope set and flip one environment variable.

Owner, admin, or developer scope. Mint a scoped production key:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/settings/api-keys \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod-messaging-r2026q3",
    "type": "secret",
    "mode": "live",
    "scopes": ["messages:read", "messages:write"]
  }'
```

Response (`201`):

```json theme={null}
{
  "data": {
    "id": "key_01H8X",
    "key": "dv_live_sk_9f3c…plaintext…",
    "name": "prod-messaging-r2026q3",
    "type": "secret",
    "mode": "live",
    "scopes": ["messages:read", "messages:write"],
    "allowed_ips": null,
    "expires_at": null,
    "created_at": "2026-09-01T10:24:00.000Z"
  },
  "meta": { "request_id": "req_9c2fa1" }
}
```

The plaintext key is returned **only on this response** (`Cache-Control: no-store`) — later reads show the masked prefix and four-character suffix. Move it into your secret manager before you close the tab.

Scope rules that save you a re-issue:

* **Pick only the channels the integration touches.** `messages:read` + `messages:write` above is a complete messaging key — a `:write` grant and the matching `:read` grant are independent, and a sender that polls its own delivery status needs both. The full scope catalog is in [Choose and scope API keys](/guides/api-keys-messaging-ccaas-cdp).
* **Name it for rotation, not for a person.** `<env>-<surface>-<cohort>` — for example `prod-messaging-r2026q3` — so the audit log shows which cohort you meant to retire. Person-names outlive the person.
* Sandbox equivalent: same call with `"mode": "test"` and a `test-…` name. The prefix swap alone changes the class.

## Step 2 — Attach the security posture to the same key

Set all three controls now, before the key first ships — none of them is harder to add afterward, but the habit that keeps keys safe is attaching them in the same session as the mint.

**IP allowlist.** Bind the key to the egress IPs your integration exits from. PATCH is replace-only — send the full list every time:

```bash theme={null}
curl -X PATCH https://api.orbit.devotel.io/api/v1/settings/api-keys/key_01H8X/allowed-ips \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "allowed_ips": ["203.0.113.42", "198.51.100.0/24"] }'
```

If the integration sits behind an outbound proxy or CDN, allowlist the egress IP the destination sees, not your dashboard machine's IP. Empty (`[]`) and `null` both clear the list, and the key has no self-lockout guard — verify each entry before you PATCH. Full rollout checklist and edge cases: [Per-API-key IP allowlist](/guides/api-key-ip-allowlist).

**Usage limits and alerts.** Put a per-minute rate limit and a monthly request quota on the key — or rely on the org defaults — plus the advisory alert threshold that flags the key before it exhausts its quota. Set and read them per key in [Per-API-key usage limits](/guides/api-key-usage-limits); add the browser-side threshold badge from [usage budgets and threshold alerts](/guides/api-key-usage-budget-alerts).

**Spend budget (money, not requests).** Persist a monthly ceiling in minor currency units on the organization — this one is shared with your whole team, unlike the per-browser badge:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/developer/budgets \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "api_key_id": "key_01H8X",
    "limit_minor": 50000,
    "currency": "USD",
    "alert_thresholds": [50, 80, 100]
  }'
```

Field-level semantics (the first time a threshold crosses, exactly one alert fires): [Per-API-key spend budgets](/guides/per-api-key-spend-budgets).

## Step 3 — Read the usage ledger before you revoke

Before you retire any key, read its live usage so you know what you are about to cut off — and so a failed rotation is a configuration miss, not a billing surprise. Two reads cover it:

```bash theme={null}
# Per-key live request metering (this minute + this UTC month)
curl https://api.orbit.devotel.io/api/v1/developer/governance \
  -H "X-API-Key: dv_live_sk_..."

# Per-key spend against its budget
curl https://api.orbit.devotel.io/api/v1/developer/budgets \
  -H "X-API-Key: dv_live_sk_..."
```

The first returns, for each key, `effective_limits` and a live `usage.requests_this_minute` / `usage.requests_this_month` pair — the same counters the rate-limit and quota surfaces enforce on. The second returns each budgeted key's `usage` against its `limit_minor`. Read them before a scheduled revocation and again after a rotation cutover; a key that still shows traffic on the old suffix after the cutoff is a sign the runbook landed somewhere other than where you aimed it. Pair with the [Request Logs console](/guides/api-request-logs-console) for the route-level view.

## Step 4 — Rotate with zero downtime

Routine rotation never needs a maintenance window. `POST …/:id/rotate` in grace mode mints the replacement and keeps the old key valid for a window you choose (1–168 hours, default 24), so you can roll the new key service by service:

```bash theme={null}
# 1. List keys first so you target the right id (secrets are never listed)
curl https://api.orbit.devotel.io/api/v1/settings/api-keys \
  -H "X-API-Key: dv_live_sk_..."

# 2. Rotate in grace mode
curl -X POST https://api.orbit.devotel.io/api/v1/settings/api-keys/key_01H8X/rotate \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"grace_period_hours": 24}'
```

Response (trimmed):

```json theme={null}
{
  "data": {
    "id": "key_01H9Q2",
    "key": "dv_live_sk_9f3c…new-plaintext…",
    "oldKeyExpiresAt": "2026-09-02T10:24:00.000Z",
    "rotationMode": "grace"
  },
  "meta": { "requestId": "req_9c2fa1" }
}
```

3. **Store the new plaintext now.** This response is the only place it ever appears.
4. **Deploy the new key** to every integration, on your own schedule, within the window. Both keys authenticate the same scopes until `oldKeyExpiresAt` — the dual-valid overlap is the entire point.
5. **Guard the window.** While a key is mid-rotation the API rejects a second rotation attempt with `409` — one rotation per grace window at a time. If you have changed course mid-window, wait for the cutoff or simply continue using the old key; it is valid until `oldKeyExpiresAt` either way. A rotation cannot be un-cut-off, but it never needs to be — that is what the window is for. (The separate `POST …/:id/schedule-revoke` cutoff, which operators set when they want a known future retire date without a rotation, IS cancellable with `POST …/:id/cancel-revoke` while it is still in the future.)
6. **Confirm the cutover in the ledger.** After the cutoff, re-read `GET /api/v1/developer/governance`: the old suffix should produce only `401`s. Anything `2xx` on it means an integration still holds the old key — flip it before the window closes.

Rollback note: if a deployment goes sideways mid-window, revert the integration to the old key value — it is still valid until `oldKeyExpiresAt` — and rotate again when you are ready. A scheduled revocation you want to abandon (`POST …/:id/schedule-revoke`) has a dedicated cancel (`POST …/:id/cancel-revoke`) while its cutoff is still in the future. The full state machine, both modes, and when to revoke instead: [API key lifecycle and rotation](/concepts/api-key-rotation-lifecycle).

## Pitfalls that bite operators in production

* **A partially scoped key fails silently for the channel you assumed.** Halting at `messages:write` because the send "worked" means the delivery-status poll 403s with `INSUFFICIENT_SCOPE` the moment a worker polls `GET /messages/:id`. Mint both `read` and `write` for a channel pair before you ship, and prove both halves over curl — the scoped call returns its real status, an out-of-scope call returns `403`.
* **A personalized share token is not an API key.** Transcript, recording, and callback share links carry their own signed actor token (see [Signed-token public share plane](/concepts/signed-token-public-share-plane)); the token is personalized to one recipient's link and bound to one object, and rotating or revoking your API keys does not touch it. A tenant-owned scope on an API key is the only thing that gates program traffic. Never ship a share token where a scope belongs, and never ship an API key where a share link belongs.
* **The moment a key becomes live is the mint, not the first send.** A `mode: "live"` key bills from its first request — there is no activation click and no warm-up window. Mint live keys inside the same change-window as the first deploy, and keep sandbox keys (`dv_test_`) out of any environment where a live route could receive them.
* **`*` and `admin` scopes are script-side only.** They never appear in the dashboard picker because a routine integration should never carry them; if your runbook asks for one, the runbook is wrong.

## Wire it into your next three reads

1. [Choose and scope API keys for messaging, CCaaS agents, and the CDP](/guides/api-keys-messaging-ccaas-cdp) — the scope table and the worked 403.
2. [API key lifecycle and rotation](/concepts/api-key-rotation-lifecycle) — the concept page this guide's runbook applies.
3. [Prefix-scoped sandbox tokens](/guides/sandbox-prefix-scoped-tokens) — the pre-launch checklist that gates go-live.
