Callens

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

ScopeGrants
ingestSubmitting calls
webhooks:manageListing, registering, updating, rotating the secret for, and testing webhook endpoints
readReading call data over MCP — as the person who minted the key
eventsSubmitting 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.

StatuserrorMeaning
401nullThe key is missing, malformed, unknown, or revoked. All four return the same message — the response cannot be used to discover which keys exist.
403insufficient_scopeValid key, wrong scope. data.required_scope names what was needed.
404webhook_endpoint_not_foundNo such endpoint.
409webhook_url_takenThat URL is already registered.
422webhook_url_refusedThe 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

BucketLimit
Per key600 / minute
Per key, burst20 / second
POST /uploads10 / minute
POST /webhooks/{id}/test6 / 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.

On this page