Appearance
Management API (v1)
Server-to-server REST API for a consumer's backend: provisioning, outbound calls, call history and control. Base path /v1, JSON in and out.
Which endpoints you need. Most integrations use a handful; the rest are there when you need them. Your scenario page shows the exact calls for your kind of product.
| Who needs it | Endpoints | Used for |
|---|---|---|
| Every integration | PUT /v1/tenants/{t}, PUT …/agents/{ref}, PUT …/channels/{ref} | The workspace, the people who take calls, the numbers |
| Every integration (recommended) | PUT /v1/webhook | Receiving events |
| Phone numbers (SIP) | GET /v1/sip-trunks · GET /v1/numbers | The trunks your SIP lines can use, and your numbers |
| Shared lines (a team) | PUT …/queues/{ref}, PUT …/queues/{ref}/members | Where calls wait and who answers |
| Mobile apps | PUT /v1/push-credentials/{provider}, PUT …/agents/{ref}/push-tokens/{deviceId} | Ringing a phone in the background |
| Outbound calls | POST /v1/tenants/{t}/calls | Placing a call for an agent |
| WhatsApp through your own Meta app | POST /v1/webhooks/whatsapp/forward | Passing Meta's webhooks on |
| Optional features | IVR flows, audio assets, reports, call history and recordings, PUT …/agents/{ref}/do-not-disturb, dashboard launch | Menus, prompts, dashboards, records |
| Housekeeping | keys, events, deleting, erasing a customer | Rotation, missed webhooks, cleanup, privacy |
Authentication
Authorization: Bearer ck_<slug>_<secret>The first API key comes with the consumer (the operator sends it to you); more are issued and revoked through Keys. Keys are stored hashed; several can be active at once, so a key is rotated without downtime. Every request is scoped to the calling consumer — its tenants, their agents, queues, channels and calls. Anything else is 404.
Rate limit: 1200 requests per minute per consumer, in fixed one-minute windows (429 with Retry-After in seconds).
Conventions
- Address entities by your own ids.
{tenantRef},{agentRef},{queueRef},{channelRef},{flowRef}are your references (1–191 characters); you never need to store Calloop's ids. PUTcreates or replaces. A field you omit is reset to its default, not kept (e.g. omittingstatussets it back toACTIVE— a tenantPUTwithoutstatuslifts a suspension). Kept when omitted: a tenant'ssettings(also whennull; send{}to clear them), an agent'sroleand do-not-disturb, a channel'scredentials, a SIP trunk's credentials andtech_prefixes, a rate sheet'scurrency.- Responses wrap the entity in a named key (
{ "tenant": … },{ "queues": [ … ] }); fields are camelCase; linked entities are returned as Calloop ids (overflowQueueId,inboundQueueId,channelId).DELETEreturns204with no body. - Calloop's numeric ids are returned in responses and events for convenience. Enum values in bodies are case-insensitive and returned uppercase (except a call detail's
recording.statusand an audio asset'sstorageProvider, which are lowercase). - Request bodies are limited to 10 MB (15 MB for audio uploads, 20 MB for rate sheet imports).
- Retry
POSTs safely with anIdempotency-Key— see Idempotency.PUT,PATCH,GETandDELETEare idempotent already. - Versions.
/v1grows by adding: new endpoints, new optional request fields, new response fields, new enum values and new event types. Ignore fields and event types you don't know, so additions never break you. Each event carries theapi_versionof its payload shape (Events). - The operator is the Calloop team that runs the platform: it creates your consumer, sends your first keys, and sets your account's plan and limits.
Idempotency
Send Idempotency-Key: <any unique string, 1–255 chars> on a POST (e.g. a UUID per logical request). If the request is retried with the same key:
| Situation | Response |
|---|---|
The first request finished with 2xx or 4xx | That same status and body again, with Idempotent-Replayed: true. Nothing runs twice |
| The first request is still running | 409 idempotency_key_in_use, Retry-After: 1 |
| Same key, different method, path or body | 422 idempotency_key_reused |
The first request failed with 5xx | The retry runs normally (a 5xx isn't stored) |
Keys are per consumer and kept for 24 hours. POST /v1/api-keys, POST /v1/signing-keys and POST /v1/webhook/secret are never replayed (the stored response would hold the new secret): a retry issues another key or rotates the webhook secret again; revoke the key you don't use, or use the newest secret. A request without the header behaves as before. A key whose first request never finished (the worker died) can be reused after 2 minutes. An empty key or one over 255 characters is a 400. The body is compared after parsing, as JSON text: the same fields in a different order count as a different body (422 idempotency_key_reused).
Errors
Every error from the /v1 endpoints in this document has one shape:
json
{ "error": { "code": "invalid_request", "message": "name is required" } }Some errors add details (for in_use: what is using the entity). A 400 about one field names it in details.field, as you sent it — a nested field with its path — so a form can show the message next to that field:
json
{
"error": {
"code": "invalid_request",
"message": "members[2].priority must be an integer between 1 and 1000",
"details": { "field": "members[2].priority" }
}
}A 400 that isn't about one field (malformed JSON, a body over no field) has no details. 409 invalid_customer on POST …/calls carries details.field: "customer.address" too. Strings are trimmed; one of only spaces counts as missing (name is required). Two exceptions: a path that isn't an endpoint gets the web server's plain 404 body, and the Meta webhook routes (Webhooks in) answer with a bare status, except 404 not_enabled (below), which uses the error shape.
An unknown outbound call's agent_ref (body), or an unknown agent_ref / tenant_ref query filter, is a 404 not_found. Any other unknown *_ref in a body or query, including a queue's members[].agent_ref and a line's owner_agent_ref, is a 400 invalid_request.
| Status | code | When |
|---|---|---|
400 | invalid_request | A field is missing, has the wrong type or is out of range (details.field names it); a *_ref in the body matches nothing; malformed JSON |
401 | unauthorized | Missing or invalid API key |
403 | consumer_suspended | The consumer is suspended |
404 | not_found | The entity doesn't exist or isn't yours |
404 | not_enabled | POST /webhooks/whatsapp (not under /v1) when direct Meta ingress isn't configured |
402 | tenant_outbound_blocked, tenant_suspended, tenant_insufficient_balance | POST …/calls for a tenant whose own account with you (Pricing your tenants) can't pay for it |
402 | outbound_blocked, account_suspended, insufficient_balance | POST …/calls your billing account can't pay for: outbound blocked (no balance, over the credit limit, an invoice overdue), suspended, or not enough balance or credit for the call's first billing block at the destination's price — message says which and how much |
403 | line_not_owned | POST …/calls from another agent's personal line — see Outbound calls |
403 | destination_blocked, destination_not_allowed, emergency_not_supported, rate_too_high | POST …/calls the platform's call rules refuse: a blocked range (high-risk, premium), a country your plan doesn't allow, an emergency short code (Calloop has no emergency calling), a destination dearer a minute than your plan allows |
409 | too_many_calls | POST …/calls while your plan's calls at once are all in use |
409 | invalid_channel, channel_disabled, unsupported_channel, invalid_agent, agent_busy, invalid_customer | POST …/calls — see Outbound calls |
409 | call_ended | POST /v1/calls/{id}/terminate on a call that already ended |
409 | call_active | Deleting data before the call ended |
409 | idempotency_key_in_use | A request with this Idempotency-Key is still running |
409 | in_use | A DELETE of something still in use — see Deleting |
409 | last_key, kid_taken | Revoking your last active API or signing key; a kid that exists — see Keys |
409 | tenant_active | Deleting a tenant that isn't SUSPENDED — see Deleting |
403 409 429 503 | self_service_off; exists, too_many_trunks, suspended, in_use, invalid_state, no_addresses; too_soon; sip_unavailable | SIP trunks |
400 403 409 | invalid_request on currency; forbidden; in_use, not_prepaid, not_open | Pricing your tenants |
413 | invalid_request | Request body over the size limit |
422 | idempotency_key_reused | The Idempotency-Key was used with a different request |
429 | rate_limited | Over the rate limit |
500 | internal_error | Unexpected failure |
503 | storage_unavailable | GET /v1/calls/{id}/recording when the store holding it can't be reached; deleting data when a recording file can't be deleted; audio-asset URLs without object storage |