Authentication
API keys, headers, scopes, and error shapes.
Presenting your key
Either header works:
Authorization: Bearer clk_{tenant_id}_{secret}X-Api-Key: clk_{tenant_id}_{secret}X-Api-Key exists because several integration platforms — n8n's generic credential among them —
cannot set an Authorization header on a webhook node. Both are treated identically.
Getting a key
Keys are minted in the Callens dashboard, under Settings → API keys. There is no endpoint for creating them: minting a credential requires a signed-in user.
Scopes
| Scope | Grants |
|---|---|
ingest | Submitting calls |
webhooks:manage | Listing, registering, updating, rotating the secret for, and testing webhook endpoints |
read | Reading call data over MCP — as the person who minted the key |
events | Submitting customer events — messages, orders, notes |
A key needs webhooks:manage to reach any /webhooks route. An ingest connector's key cannot
manage your webhook endpoints, and that separation is deliberate.
ingest and events are separate for the same reason. A key that can only submit calls cannot
submit customer events, and a key that can only submit events cannot submit calls. Whether customer
events are accepted at all also depends on your plan.
An events key belongs on a server. Its responses say whether a phone number or an email address is
already one of your customers, so it must never be exposed to a browser or to any anonymous client.
See sending customer events.
read is the odd one out and worth reading about before you grant it. It reaches exactly one
endpoint — the MCP server — and unlike the other three it is not an organization-wide
grant: a read key acts as the member who minted it and sees only what that person sees. It also
stops working the moment they leave.
The webhooks_enabled flag
This is a different per-key property from the webhooks:manage scope above, and the two are
easy to conflate — several pages in this documentation say "if your key has webhooks enabled,"
and this is the flag they mean.
webhooks_enabled is set on a key when it is minted (or later, from the dashboard). It does one
thing: on a key with the flag set, POST /calls requires provider_call_id and
idempotency_key, so that the call.completed event this call produces can always be joined back
to your record. It grants no route access — a key can have webhooks_enabled with no scopes at
all, or webhooks:manage with the flag off.
Do not read the flag as "this key controls whether events are delivered." It does not — delivery is decided per organization, not per key. See correlation identifiers for what that means for a key that lacks the flag.
Errors
Domain and scope errors use one envelope:
{ "success": false, "message": "…", "error": "insufficient_scope", "data": { "required_scope": "webhooks:manage" } }Request-body validation failures do not use this envelope. A 422 produced by a field
failing a validation rule — a missing provider_call_id, an ended_at before started_at — is
rendered by Laravel's own validator, not by Callens's error handler, and has no success, no
error, and no data:
{ "message": "The provider call id field is required.", "errors": { "provider_call_id": ["The provider call id field is required."] } }message is just the first entry from errors, restated at the top level (with "(and N more
errors)" appended if there were others) — errors is the one to parse, keyed by field name, each
value a list of messages for that field.
| Status | error | Meaning |
|---|---|---|
| 401 | null | The key is missing, malformed, unknown, or revoked. All four return the same message — the response cannot be used to discover which keys exist. |
| 403 | insufficient_scope | Valid key, wrong scope. data.required_scope names what was needed. |
| 404 | webhook_endpoint_not_found | No such endpoint. |
| 409 | webhook_url_taken | That URL is already registered. |
| 422 | webhook_url_refused | The URL is not https, or does not resolve to a public address. |
| 422 | (not this envelope) | The request body failed a validation rule. See the shape above — errors carries the per-field messages, not data. |
Rate limits
| Bucket | Limit |
|---|---|
| Per key | 600 / minute |
| Per key, burst | 20 / second |
POST /uploads | 10 / minute |
POST /webhooks/{id}/test | 6 / minute |
The test route has its own budget because it makes Callens issue an outbound request to an
address you choose. That 6/minute limit stacks on top of the 600/minute and 20/second-burst
limits above — both buckets are checked on every call to that route, and whichever is tighter
binds. In practice that is always the 6/minute figure, so plan retry/backoff around it for this
route specifically. POST /uploads carries its own 10/minute ceiling and stacks the same way, so a
client batching presign requests meets that number rather than the 600/minute one.
Rate limits has the full list, what each bucket is sized for, and how to tell a throttle apart from an allowance refusal on the same status.