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 Call Lens 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 |
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.
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 two 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 Call Lens'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 Call Lens 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.