API keys

Server-to-server calls carry an API key in the Authorization 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.
Request bodies are typed models, not dicts — the generated client calls 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 by organization_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 an Authorization 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:
Tickets are single-use and expire in 30 seconds, so mint one per connection and a fresh one for every reconnect. It goes in 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:
  • StripeStripe-Signature HMAC against STRIPE_WEBHOOK_SECRET. A bad signature is a 403.
  • 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 answers 204 whatever 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.
Verified-but-uninteresting events are acknowledged so the sender stops retrying.