Authentication
Every voepy API request is authenticated with a tenant-scoped API key sent as a Bearer token. This guide covers how auth works, the full key lifecycle (create, list, retrieve, revoke, rotate), the scopes that gate each endpoint, and how to confirm which account a key belongs to.
All paths under
/v1/.Authorization: Bearer voepy_live_<key>required on every request.
How authentication works
Send your API key in the Authorization header on every call:
GET /v1/account
Authorization: Bearer voepy_live_aB3dEf7hJk2mNp5qRsTuVwXyZ...
Three things to internalize:
- Keys are tenant-scoped. A key identifies exactly one account. Every resource you can read or write through it — calls, numbers, recordings, billing — is filtered to that tenant. There is no cross-tenant access.
- Keys carry scopes. Each key holds a set of permission strings
(e.g.
calls:write,billing:read) that gate which endpoints it can reach. A request to an endpoint your key isn't scoped for returns403. See Scopes below. - Keys are server-side secrets. Treat a key like a password. Never embed one in a browser, mobile app, or any client you don't control. If a browser needs to reach voepy, proxy the request through your own backend so the key never leaves your servers.
The plaintext key looks like voepy_live_ followed by a long random
string. We store only a salted hash — we cannot show you a key again
after it's issued, and we cannot recover a lost one. If a key leaks or
goes missing, rotate it.
The API key resource
List and retrieve calls return the key's metadata — never the secret:
{
"id": "9f1c0b7e-2a4d-4e3a-9c11-5d6e7f8a9b0c",
"name": "production-backend",
"prefix": "voepy_live_aB3dEf7h",
"scopes": ["calls:write", "calls:read", "billing:read"],
"last_used_at": "2026-06-18T09:14:02.120Z",
"last_used_ip": "203.0.113.40",
"expires_at": null,
"revoked_at": null,
"created_at": "2026-05-12T14:32:18.000Z"
}
| Field | Type | Notes |
|---|---|---|
id | UUID | The key record's identifier. Use it in the path to retrieve or revoke. Not the secret. |
name | string | Human label you chose at creation. |
prefix | string | First several characters of the plaintext (e.g. voepy_live_aB3dEf7h). Safe to display — use it to recognize a key in logs without revealing the rest. |
scopes | string[] | Permissions this key carries. |
last_used_at | timestamp | null | When the key last authenticated a request. null if never used. |
last_used_ip | string | null | Source IP of the last request. null if never used. |
expires_at | timestamp | null | When the key auto-expires. null means it never expires. |
revoked_at | timestamp | null | When the key was revoked. null means it's still active. |
created_at | timestamp | When the key was issued. |
prefix and id are the two values safe to log or surface in your own
UI. The full plaintext is shown exactly once, at creation.
Create a key
POST /v1/api-keys/
Content-Type: application/json
Idempotency-Key: <uuid>
{
"name": "production-backend",
"scopes": ["calls:write", "calls:read", "billing:read"],
"expires_at": "2027-01-01T00:00:00Z"
}
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | required | 1–120 chars. A label only you see. |
scopes | string[] | ["*"] | Permissions to grant. Omit to grant all scopes (*). Prefer the narrowest set the key needs. |
expires_at | timestamp | null | null | Optional auto-expiry. Omit or send null for a non-expiring key. |
Response is 201 Created with the key metadata plus the one-time
plaintext in api_key_plaintext:
{
"id": "9f1c0b7e-2a4d-4e3a-9c11-5d6e7f8a9b0c",
"name": "production-backend",
"prefix": "voepy_live_aB3dEf7h",
"scopes": ["calls:write", "calls:read", "billing:read"],
"last_used_at": null,
"last_used_ip": null,
"expires_at": "2027-01-01T00:00:00Z",
"revoked_at": null,
"created_at": "2026-06-18T09:00:00.000Z",
"api_key_plaintext": "voepy_live_aB3dEf7hJk2mNp5qRsTuVwXyZ..."
}
api_key_plaintextappears in this one response and never again. Copy it straight into your secret store (env var, vault, secrets manager). If you lose it, you cannot retrieve it — you'll have to rotate.
Always send Idempotency-Key
Creating a key is a mutating operation. Send a unique Idempotency-Key
header so a retried request (after a network blip) returns the original
key instead of minting a second one. Use one UUID per logical create.
List keys
GET /v1/api-keys/
?page=1
&page_size=20
Returns a paginated envelope of key metadata for the authenticated tenant, newest first:
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": "9f1c0b7e-2a4d-4e3a-9c11-5d6e7f8a9b0c",
"name": "production-backend",
"prefix": "voepy_live_aB3dEf7h",
"scopes": ["calls:write", "calls:read", "billing:read"],
"last_used_at": "2026-06-18T09:14:02.120Z",
"last_used_ip": "203.0.113.40",
"expires_at": null,
"revoked_at": null,
"created_at": "2026-05-12T14:32:18.000Z"
},
{
"id": "1d2e3f4a-5b6c-7d8e-9f0a-1b2c3d4e5f60",
"name": "ci-pipeline",
"prefix": "voepy_live_Zz9yXw8v",
"scopes": ["calls:read"],
"last_used_at": null,
"last_used_ip": null,
"expires_at": null,
"revoked_at": "2026-06-01T11:00:00.000Z",
"created_at": "2026-05-20T08:00:00.000Z"
}
]
}
Revoked keys stay in the list (with revoked_at set) so you keep an
audit trail. page is 1-indexed and page_size defaults to 20 (max
200); follow next until it's null for the last page.
Retrieve one key
GET /v1/api-keys/{id}/
{id} is the key's id (the UUID), not the plaintext. Returns the same
metadata object as above. There is no endpoint that returns the secret —
by design.
Revoke a key
DELETE /v1/api-keys/{id}/
Returns 204 No Content. The key is immediately rejected on every
subsequent request and its record is marked with revoked_at. Revocation
is permanent — you cannot un-revoke; issue a new key instead.
Rotating a key
There is no in-place "regenerate." Rotation is create-then-revoke, with a brief overlap so you never lock yourself out:
- Create a new key with the same scopes (Create a key).
- Deploy the new key to the service (env var / secret store) and
confirm traffic is flowing on it — watch the new key's
last_used_atstart advancing via retrieve. - Revoke the old key once nothing is using it.
Rotate on any suspected leak, when an employee with key access leaves, or
on a regular schedule for long-lived keys. Setting expires_at at
creation makes rotation a forcing function.
Scopes and permissions
Each endpoint requires a specific scope. A key reaches an endpoint only
if its scopes include that scope (or the wildcard *). Requests that
authenticate but lack the required scope return 403.
Scopes follow a resource:action shape. The ones the API enforces today:
| Scope | Grants |
|---|---|
calls:read | Read calls, legs, recordings, transcripts. |
calls:write | Place calls and run call control actions. |
account:read | Read the account via GET /v1/account. |
usage:read | Read usage / ledger history. |
billing:read | Read balances, invoices, payment methods. |
billing:write | Top up, change auto-recharge, manage payment methods. |
webhooks:read | Read webhook subscriptions. |
webhooks:write | Create and manage webhook subscriptions. |
connection:read | Read connection / routing configuration. |
audit:read | Read the audit log. |
api_keys:read | List and retrieve API keys. |
api_keys:write | Issue new API keys. |
api_keys:revoke | Revoke API keys. |
* | All scopes. Convenient, but the opposite of least privilege. |
Grant the narrowest set a service actually needs. A telephony worker
that only dials out wants calls:write (and maybe calls:read) — not
billing:write or the API-key management scopes.
Scopes on a key are fixed at creation. To change what a key can do, create a replacement with the new scope set and revoke the old one (rotate).
For human team members, permissions are governed by roles rather than raw scopes — roles map to the same scope set under the hood. See Team & roles for how roles, members, and API keys relate.
Check the authenticated identity
To confirm which tenant a key belongs to — and see balance and quota in
the same call — hit GET /v1/account. This is the canonical "whoami /
ping" for a key (requires account:read).
GET /v1/account
Authorization: Bearer voepy_live_aB3dEf7hJk2mNp5qRsTuVwXyZ...
{
"tenant": {
"id": "7c8d9e0f-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"name": "Acme Voice",
"slug": "acme-voice",
"status": "active",
"currency": "USD",
"created_at": "2026-04-01T00:00:00.000Z",
"updated_at": "2026-06-18T09:00:00.000Z"
},
"balance": {
"balance_cents": 4250,
"credit_limit_cents": 0,
"auto_recharge_threshold_cents": 1000,
"auto_recharge_amount_cents": 5000,
"updated_at": "2026-06-18T09:14:02.120Z"
},
"quota": {
"daily_spend_cap_cents": 50000,
"monthly_spend_cap_cents": 1000000,
"concurrent_call_limit": 50
}
}
A 200 here means the key is valid and the tenant is reachable. A 401
means the key is missing, malformed, expired, or revoked. Money fields
are in cents; see Billing for what balance and
quota mean in practice.
Security best practices
- Server-side only. Never ship a key in browser, mobile, or desktop client code. Proxy through your backend.
- One key per environment and service. Separate keys for prod, staging, and each worker. A leak then has a blast radius of one service, and you can revoke it without taking everything else down.
- Restrict scopes. Grant the minimum each key needs. Avoid
*outside of trusted, full-access tooling. - Store keys in a secret manager. Env vars, a vault, or your platform's secrets store — never in source control, tickets, or chat.
- Set
expires_aton long-lived keys so a forgotten key can't live forever, and rotate on a schedule. - Rotate immediately on any suspected leak — committed to a repo, pasted in a log, or held by a departing teammate.
- Watch
last_used_at/last_used_ip. Unexpected activity or a key that should be idle but isn't is a signal to revoke and rotate.
Troubleshooting
| HTTP | Code | What it means | What to do |
|---|---|---|---|
| 401 | unauthenticated | No key, malformed header, or the key is expired/revoked. | Send Authorization: Bearer voepy_live_<key>. Confirm the key isn't expired or revoked via retrieve; rotate if needed. |
| 403 | permission_denied | The key authenticated but lacks the required scope. | Check the endpoint's required scope. Issue a key with the right scopes (rotate). |
| 404 | not_found | The key id in the path doesn't belong to your tenant. | Keys are tenant-scoped — verify you're authenticating as the tenant that owns the key. |
All errors share the standard envelope:
{
"error": {
"code": "permission_denied",
"message": "This API key is not authorized for scope 'billing:write'."
}
}
Full list and recovery steps: Error reference.
Next
- Getting started — grab your first key and make a call.
- Team & roles — manage human members and how their roles map to scopes.
- Conventions — auth header, ID formats, pagination, idempotency in one place.
- Error reference — every code the API can return.
