# Stream API keys, signing keys and webhook deliveries

> How stream API keys, scopes, environments, signing keys, webhook endpoints and event deliveries work on the Developers screen.

The Stream **Developers** screen is the single place where you mint the
credentials that talk to ClutchCall Stream, generate the keys that sign
playback tokens and webhook deliveries, register webhook endpoints, and
inspect what has been delivered.

Everything the screen renders comes from four list procedures, all scoped
to the current org:

| Panel                     | Procedure                            | Record                |
| ------------------------- | ------------------------------------ | --------------------- |
| API keys                  | `streams.apiKeys.list`               | `stream_api_key`      |
| Signing keys              | `streams.signingKeys.list`           | `stream_signing_key`  |
| Webhook endpoints         | `streams.webhooks.list`              | `stream_webhook`      |
| Recent event deliveries   | `streams.eventDeliveries.list`       | `webhook_delivery`    |

The first three load once per visit. The deliveries panel requests the
25 most recent rows and refreshes every 30 seconds while the screen is
open.

## API keys and scopes

A stream API key is a bearer token that your **server** presents to the
control plane. Each key row carries:

| Field           | Meaning                                                             |
| --------------- | ------------------------------------------------------------------- |
| `name`          | The label you gave the key at creation. Free text, for humans only. |
| `token_preview` | The stored leading fragment of the token. See below.                |
| `scopes`        | The capability scope strings attached to this key.                  |
| `env`           | `live` or the test environment.                                     |
| `created_at`    | When the key was minted.                                            |
| `last_used_at`  | Last successful authentication, or `never`.                         |
| `revoked_at`    | Set once the key is revoked. A revoked key authenticates nothing.   |

`scopes` is a list, not a single value, and the console renders one tag
per entry. Scope is enforced per procedure: the control plane rejects a
call when the presented key does not carry the scope that the called
procedure requires. Mint one key per job and give it only the scopes that
job needs — a key scoped to what it actually does limits the blast radius
when it leaks.

Scopes are fixed at creation time. There is no edit control on this
screen: to change what a key can do, create a replacement key with the
scopes you want, move your callers onto it, then revoke the old one.

## Live and test environments

Every key belongs to exactly one environment, and the `env` tag on the
row tells you which. The console colours `live` green and the test
environment amber, because the two are the mistake that costs the most:
a test key in production stops working, and a live key in a test harness
touches real inputs, real assets and real billable delivery.

The environments are separate credential namespaces. A key is not
promoted from test to live — you mint a second key in the environment you
want. The rest of the screen (signing keys, webhook endpoints,
deliveries) is listed for the environment that the Stream shell is
currently showing.

## Key creation, preview-only storage, and rotation

**Create API key** mints a key and returns the full token string exactly
once, at creation. After that the control plane stores only a hash of the
token plus a short leading fragment (a dozen or so characters) in
`token_preview`.

That is why the key table shows the preview followed by mask characters
and offers neither a reveal nor a copy control:

- There is nothing to reveal. The cleartext is not retained anywhere, so
  no control could produce it.
- There is nothing useful to copy. The preview is a fragment that can
  never authenticate, and putting it on the clipboard only invites
  pasting it somewhere as though it were a token.

The preview exists so that you can match a row in this table against a
token you already hold in a secret store or a log line, and so that
revoking the right key is unambiguous.

**Rotation is create-then-revoke.** A lost token is not recoverable:
create a new key, deploy it, confirm the new key's `last_used_at` is
moving, then use **Revoke** on the old row. Revoking is the only
destructive action on a key; the row stays in the list afterwards and
renders as `revoked` instead of offering a revoke control, so the history
of which key was retired and when is preserved.

## Signing keys and their two uses

Signing keys are asymmetric keys held by the control plane. They are not
bearer credentials and are never presented as one. Each key exists for
exactly one **use**, and the console labels the row accordingly:

| `use`      | Console label          | What it signs                                      |
| ---------- | ---------------------- | -------------------------------------------------- |
| `playback` | Signed playback URLs   | Short-lived playback tokens that you mint per viewer. |
| webhook    | Webhook signature      | Every webhook delivery ClutchCall POSTs to your endpoints. |

Each row shows the key id and its algorithm (`alg`) in mono type, plus
the creation date. You reference the key id when minting a playback token
(see below), and you use the matching public key when verifying a webhook
delivery.

Keep the two uses on separate keys. They have different exposure: a
playback key signs tokens that go out to browsers many times a second,
while a webhook key signs traffic that only ever goes to endpoints you
control. Retiring one should not invalidate the other.

## Signing key lifecycle states

`status` drives the status indicator on each row:

| `status`   | Indicator    | What it means                                                                                 |
| ---------- | ------------ | --------------------------------------------------------------------------------------------- |
| `active`   | ready        | The key signs new tokens and deliveries. This is the key you name in `signPlayback`.          |
| `rotating` | processing   | A successor exists. The key no longer signs new material, but signatures it produced are still being honoured while they age out. |
| `retired`  | idle         | Signs nothing and verifies nothing new. Kept in the list for audit.                           |

The **Retire** control appears only on rows whose status is `active` —
a rotating or retired key has nothing left to retire. Because retiring is
offered only from `active`, the safe order is: generate the successor with
**Generate signing key**, point your minting code at the new key id, and
retire the old key once nothing still references it.

For playback keys, "nothing still references it" means every token that
the old key signed has passed its `expiresIn`. For webhook keys, it means
your verifier has been taught the new public key.

## Minting a signed playback token

Signed playback tokens are minted on your server, with the SDK, against a
signing key whose use is `playback`:

```ts
import { signPlayback } from "@clutchcall/stream";

// short-lived, viewer-scoped token
const token = signPlayback({
  playbackId: "pb_8Kq2Rf4mT0",
  keyId: "sgn_4f8a",
  expiresIn: "4h",
  audience: viewer.id,
});

// player picks it up automatically
// ?token=eyJhbGciOiJFZERTQ…
```

| Argument     | Meaning                                                                                   |
| ------------ | ----------------------------------------------------------------------------------------- |
| `playbackId` | The playback id the token authorises. A token is good for that one target.                 |
| `keyId`      | The signing key to sign with — the id shown on the signing key row. It must be `active`.   |
| `expiresIn`  | Token lifetime, as a duration string. Keep it as short as your session length allows.      |
| `audience`   | The viewer this token was minted for, so a leaked token is traceable and not universally replayable. |

`signPlayback` returns the token string. Append it to the playback URL as
`?token=…`; the player reads it from the URL and presents it on playback
without further wiring.

Never run this in the browser. The signing key is what makes a token
valid, and minting is a server-side operation for the same reason the API
key is: whatever can mint tokens can mint them for any viewer and any
lifetime.

## Webhook endpoints and event types

**Add endpoint** registers a URL that ClutchCall POSTs events to. Each
endpoint row shows:

| Field                  | Meaning                                                        |
| ---------------------- | -------------------------------------------------------------- |
| `url`                  | The destination. Deliveries are signed POSTs.                  |
| `event_types`          | The set of event types this endpoint is subscribed to. The table shows the count. |
| `last_status_code`     | The HTTP status of the most recent delivery attempt, or `—` if nothing has been attempted. |
| `status`               | `active`, `paused`, or another non-healthy state.              |
| `consecutive_failures` | Attempts that have failed in a row since the last success.     |

An endpoint receives only the event types it is subscribed to, so the
count column is the quickest check when an event you expected never
arrived: a healthy endpoint subscribed to the wrong set looks perfectly
fine.

Every delivery is signed with the signing key whose use is webhook
signature. Verify that signature on your endpoint before you act on the
payload, using the algorithm shown on the signing key row — an unsigned
or unverified POST to a public URL is an open write endpoint. While a
webhook key is `rotating`, accept signatures from both the rotating key
and its `active` successor, then drop the old one once you retire it.

**Delete** removes an endpoint. Deleting stops future deliveries to that
URL; it does not remove the delivery rows already logged for it.

## Webhook endpoint health states

The console collapses `status` and `consecutive_failures` into one
indicator:

| Rendered           | Condition                                                    |
| ------------------ | ------------------------------------------------------------ |
| **Healthy**        | `status` is `active` and `consecutive_failures` is zero.     |
| **Paused**         | `status` is `paused`. Nothing is being delivered.            |
| **Failing (`n`x)** | Anything else. `n` is `consecutive_failures`.                |

An `active` endpoint with a non-zero failure count shows as failing, not
as healthy — a key detail, because the endpoint is still enabled and
still accruing attempts. Read `last_status_code` next: a 4xx usually
means the payload or the signature check is being rejected, while a 5xx
or a missing code points at the endpoint itself being down. A successful
delivery resets the counter and the row returns to Healthy.

## Delivery states, retries, and resending

The **Recent event deliveries** table is the delivery log — the 25 most
recent attempts, refreshed every 30 seconds. There is no separate log
explorer, so this panel is where a delivery question gets answered.

Each row carries the `event_type`, the `obj_id` of the object the event is
about (a live input, an asset, and so on), the response, and the
timestamp.

The response column has three renderings, and the distinction matters:

| Rendering       | Condition                                                                 |
| --------------- | ------------------------------------------------------------------------- |
| `2xx OK`        | `response_code` is in the 2xx range. Your endpoint accepted the delivery. |
| `<code> ERR`    | A response was received and it was not 2xx.                               |
| `— Queued`      | `status` is `pending`, or there is no `response_code` yet and the delivery has not been marked `failed`. |

**Queued is not failed.** A delivery that has been accepted for delivery
but has no response yet renders neutral rather than red, so a burst of
in-flight events does not read as an outage. Only a delivery that has
actually come back non-2xx, or that is marked `failed`, is an error.

Deliveries that fail are retried automatically with backoff — successive
attempts are spaced further apart rather than hammering a dead endpoint —
and each failed attempt increments the endpoint's
`consecutive_failures`, which is what eventually moves the endpoint row
out of Healthy.

**Resend** on a delivery row asks for that event to be delivered again. It
queues a fresh attempt; it does not cancel, replace, or reprioritise an
attempt that is already queued or in flight. Resending a delivery that is
still showing `— Queued` therefore leaves you with two attempts for the
same event, so wait for a response before resending. Because a resend is
an additional delivery of the same event, your endpoint handlers should be
idempotent on the event id.

## Related

- [Authentication](/concepts/authentication) — API keys, relay tokens, and signing-key rotation across the platform
- [Telemetry](/platform/telemetry) — operational data streams and where to scrape them
