voepy

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:

  1. 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.
  2. 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 returns 403. See Scopes below.
  3. 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"
}
FieldTypeNotes
idUUIDThe key record's identifier. Use it in the path to retrieve or revoke. Not the secret.
namestringHuman label you chose at creation.
prefixstringFirst 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.
scopesstring[]Permissions this key carries.
last_used_attimestamp | nullWhen the key last authenticated a request. null if never used.
last_used_ipstring | nullSource IP of the last request. null if never used.
expires_attimestamp | nullWhen the key auto-expires. null means it never expires.
revoked_attimestamp | nullWhen the key was revoked. null means it's still active.
created_attimestampWhen 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"
}
FieldTypeDefaultNotes
namestringrequired1–120 chars. A label only you see.
scopesstring[]["*"]Permissions to grant. Omit to grant all scopes (*). Prefer the narrowest set the key needs.
expires_attimestamp | nullnullOptional 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_plaintext appears 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:

  1. Create a new key with the same scopes (Create a key).
  2. 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_at start advancing via retrieve.
  3. 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:

ScopeGrants
calls:readRead calls, legs, recordings, transcripts.
calls:writePlace calls and run call control actions.
account:readRead the account via GET /v1/account.
usage:readRead usage / ledger history.
billing:readRead balances, invoices, payment methods.
billing:writeTop up, change auto-recharge, manage payment methods.
webhooks:readRead webhook subscriptions.
webhooks:writeCreate and manage webhook subscriptions.
connection:readRead connection / routing configuration.
audit:readRead the audit log.
api_keys:readList and retrieve API keys.
api_keys:writeIssue new API keys.
api_keys:revokeRevoke 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_at on 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

HTTPCodeWhat it meansWhat to do
401unauthenticatedNo 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.
403permission_deniedThe key authenticated but lacks the required scope.Check the endpoint's required scope. Issue a key with the right scopes (rotate).
404not_foundThe 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