API keys
Server-to-server calls carry an API key in theAuthorization header:
X-API-Key: sk-eesi-… is accepted too, for clients that reserve
Authorization for something else. Everything in these docs uses the bearer
form.
Keys are scoped to an organization, shown in full exactly once at creation, and
stored hashed — only the last four characters are recoverable afterwards.
Creating a key, and un-archiving one, need a signed-in session — do both under
Settings → API keys. A key cannot mint another key or reactivate one,
including itself: otherwise a leaked key would survive being revoked, both by
minting a replacement and by undoing the revocation. The SDK has no method for
either, and the REST endpoints answer 403 to a key.
A key can archive keys, which is why the SDK keeps that one — containing a
suspected leak should never require finding someone who can log in.
model_dump() on whatever you pass, so a plain dict raises before the request
is sent.
User sessions
The dashboard authenticates through Clerk. Browser requests carry a session token; the backend resolves it to a user and their selected organization.Organization scoping
Every org-scoped resource is filtered byorganization_id at the query level.
An id from a request body never implies ownership — referenced resources are
re-fetched under the caller’s org and rejected with a 404 if they belong to
someone else. This is tenant isolation, not politeness.
Browser clients
Don’t ship an API key to a browser — and a browser cannot put anAuthorization
header on a WebSocket handshake anyway, so the credential would have to ride the
URL, and URLs get logged.
Mint a ticket over HTTPS instead, where the header is not logged, and hand the
socket something that dies on first use:
token — the gateway reads
exactly three credential channels (the Authorization header, ?api_key=,
?token=) and a ticket under any other name is a 401.
A carrier’s media socket is the one exception, and it does not use a ticket: the
signed capability in its URL is the whole of its authentication, because the
carrier has no session of any kind. See Telephony.
Webhook signatures
Inbound webhooks are verified, not authenticated:- Stripe —
Stripe-SignatureHMAC againstSTRIPE_WEBHOOK_SECRET. A bad signature is a403. - Telnyx — Ed25519 over
{telnyx-timestamp}|{raw body}, against the public key stored on the telephony configuration, with a five-minute timestamp tolerance so a captured delivery cannot be replayed. Every phone webhook answers204whatever it decides, including a refusal: a carrier retries anything that is not a 2xx, and the responses must not tell an unauthenticated caller which guess was closest.