# Keys & scoping

Two kinds of key, one hard rule: **money is server-authoritative.**

| | `sk_live_` (server) | `pk_live_` (public) |
|---|---|---|
| Behavioral events | ✓ | ✓ (rate-limited) |
| `payment` events | ✓ | ✗ |
| `/v1/objects` (subscriptions) | ✓ | ✗ |
| Read APIs | ✓ | ✗ |

`pk_live_` keys are designed to ship inside binaries: Expo bundles, Swift
apps, Electron builds. Assume they **will** be extracted; that's fine,
because a public key can only ever write behavioral events. Nobody who
pulls your key out of an IPA can touch the revenue or MRR you show your
investors.

Revenue never legitimately originates client-side: your server (or your
biller's webhook) is where money truth lives, so that's the only place that
can write it.

Rotation: revoke in the dashboard; propagation to the edge takes ≤60
seconds. Keys are shown in full exactly once, at creation.

## Test and live are separate environments

Every key also carries an **environment**, baked into the same prefix:
`sk_test_` / `pk_test_` for test, `sk_live_` / `pk_live_` for live. The
capability rules above hold unchanged inside each one: an `sk_test_` key
writes `payment` events in test, and a `pk_test_` key is behavioral-only and
just as safe to ship as `pk_live_`.

What the prefix buys you is **isolation**. Test keys write to, and test reads
come from, a different `project_id` than live. The boundary is structural,
not conventional: a request is routed to its plane by the key's own material,
server-side, so a test key can never touch live data and a live key can never
touch test. There is no environment flag to pass, and none to get wrong.

Test data follows the same retention as live: events age out at 90 days,
payments are kept. The test environment is
org-shared, not per-developer, and provisioned lazily: it comes into being
the first time you switch an app into test mode or mint a test key, and stays
empty until you send it a first event.

To read test metrics over the API, an `sk_test_` key is scoped to the app's
test plane, so the project is implied. A `dg_test_` + `metrics:read`
[control token](/docs/api/apps) is org-scoped, so it names the test project
explicitly: `?project=proj_...` (the test project's id). A `dg_live_` token
reads live projects only (see below).

## Control tokens are a third class

`sk_live_` and `pk_live_` are **app-scoped data-plane** keys: they send events
and read metrics for one app. Managing apps and keys themselves is a separate
credential: the **org-scoped** `dg_` control token, which mints those keys
over HTTP. It carries explicit scopes and never touches ingest. See the
[Control API](/docs/api/apps).

Control tokens are **plane-confined**, the same as data keys. A `dg_live_` token
reads live metrics and mints live keys; a `dg_test_` token, minted from the
dashboard in test mode, reads only test metrics and mints only test keys.
Neither crosses into the other plane, and a `dg_test_` token cannot create apps
(apps are created with a live token, and their first key is an `sk_live_`).
