# External agent media credentials

> What the voice agent key, secret, handle and /media/ endpoint path authenticate, and how they differ from realtime app keys.

An **externally-hosted voice agent** runs on your infrastructure, not inside
TeleQuick. For the gateway to hand media to it, the agent needs its own
credential pair. Those credentials are what the **Voice agent keys** table on
**Apps & keys** lists: one row per external agent, with the agent's name and
handle, its key, a masked secret, a status, and the `/media/` endpoint path the
media transport uses.

The table is read-only. It is there so that an operator auditing "what
credentials does this org have issued" sees the voice-side credentials as well
as the realtime app keys — the two live in different tables and different
engine registries, and neither one authenticates the other.

## What an external agent credential authenticates

An external agent credential authenticates the **`/media/` transport only**.
It is the credential your agent process presents so that the gateway will
accept it as the media peer for calls routed to that agent.

It does not authenticate:

- Pusher-compatible channel connections (those use a realtime app key/secret).
- The realtime HTTP API.

The console reads the rows with `mediaApps.listForOrg({ orgId })`, which is
org-scoped: the query only runs once an org is selected, and it returns the
credentials issued for that org.

## Handle, key, secret and media path

Each row carries the following fields, exactly as the procedure returns them:

| Field          | Column      | What it is                                                                                   |
| -------------- | ----------- | -------------------------------------------------------------------------------------------- |
| `agentName`    | Agent       | The display name of the external agent the credential belongs to. Shown as `—` when unset.    |
| `handle`       | Agent       | The agent's stable handle, rendered under the name as `handle <value>`. Shown as `—` when unset. |
| `key`          | Key         | The public half of the pair. Safe to log and to identify the credential by.                   |
| `secretMasked` | Secret      | A masked tail of the secret. The listing never returns the full secret.                       |
| `status`       | Status      | Whether the credential currently authenticates. See below.                                    |
| `mediaPath`    | Endpoint    | The `/media/` path this credential is bound to.                                               |

Two consequences matter in practice:

- **Do not reconstruct the media path.** The console prints `mediaPath`
  verbatim as the server returns it. Read it from the credential rather than
  assembling it from the handle or the key on your side.
- **Do not expect to recover the secret here.** Secrets are sealed at rest and
  are not returned by the API after creation, so this screen can only show you
  the masked tail. If nobody has the full secret, the credential has to be
  rotated where it is managed, not read back.

## Status values and what disables a credential

The status column has two rendering states:

- `active` — shown as a live "active" pill. The credential authenticates.
- **anything else** — the row is dimmed and the pill shows the literal status
  value returned by the server. The console treats *any* value other than
  `active` as disabled.

So if you are debugging an external agent that will not connect, a dimmed row
means the credential itself is off; the exact word in the pill is the
server-side reason, and it is passed through unchanged rather than mapped to a
fixed list. Rotating or disabling happens outside this screen, so a status you
did not expect here reflects a change made in the voice configuration.

## Why it is not interchangeable with a realtime app key

Both card types on this screen show a key, a masked secret and a status, which
makes them look like the same object. They are not:

|                        | Realtime app key                                  | External agent media credential          |
| ---------------------- | ------------------------------------------------- | ---------------------------------------- |
| Procedure namespace    | `realtime.apps.*`                                 | `mediaApps.listForOrg`                   |
| Storage                | Realtime app table / engine registry              | A separate table and engine registry     |
| Authenticates          | The HTTP API and client channel connections       | The `/media/` transport                  |
| Identified by          | `app_id` plus key                                 | Agent `handle` plus key                  |
| Endpoint shown         | (none — clients connect with the key)             | `mediaPath`                              |
| Managed from this page | Yes — create, rename, rotate, delete              | No — read-only                           |

Presenting a realtime app key to the `/media/` transport, or an agent
credential to a channel connection, fails: they are checked against different
registries.

## Where these are created and rotated

External agent credentials are created, rotated and disabled under
**Voice AI → External agents**. Nothing on **Apps & keys** mutates them —
there is no row action menu on the Voice agent keys table, by design, so that
a credential in active use on a call path cannot be rotated from an audit
screen.

The Voice agent keys card is also hidden entirely when the org has no external
agent credentials, so its absence means "none issued", not "failed to load".

## Related

- [Authentication](/concepts/authentication) — API keys, relay tokens, and which surface each one covers
