Skip to content

Rate limits

Requests are metered with distributed token buckets. Several apply at once, and the strictest one wins.

BucketApplies toQuota
clientEvery data-plane request, per credential120/min, adjustable per credential
companyEvery data-plane request, across all credentials of the company600/min
transcriptTranscript reads only, on top of the two above60/min
oauth-clientPOST /oauth/token, per client id10/min, burst 5
oauth-ipPOST /oauth/token, per source IP, and GET /oauth/authorize30/min, burst 10
oauth-grantPOST /oauth/token with grant_type=authorization_code or refresh_token, per connection10/min, burst 5
oauth-revokePOST /oauth/revoke, per connection10/min, burst 5

A company may hold up to 20 active company credentials; user credentials do not count toward that limit, and every credential of the company draws from the same company quota. Call GET /v1/context to read the credential quota actually in effect for yours.

The oauth-grant bucket belongs to one connection made by signing in with OAuth, so people behind the same address never share it, and such a grant is not charged to oauth-ip. A grant that does not match any connection, for example an unknown code, is charged to oauth-ip instead. The metadata documents under /.well-known/ have no rate limit.

POST /oauth/revoke is charged the same way. A token that is still valid for a connection, whether its refresh token or one of its access tokens, is charged to that connection’s oauth-revoke bucket; any other token is charged to oauth-ip.

Every response carries the current state. RateLimit-Policy lists every bucket the request consumed; RateLimit reports the single one closest to exhaustion:

RateLimit-Policy: "client";q=120;w=60, "company";q=600;w=60
RateLimit: "client";r=118;t=1

q is the quota, w the window in seconds, r what is left and t the seconds until another token. RateLimit names whichever bucket is holding the fewest tokens: normally your credential’s, the transcript one on transcript reads, and the company one once sibling credentials are spending it. Pace against the ceilings in RateLimit-Policy — the bucket named in one reading is not the only one binding you.

r counts tokens in the bucket, whose capacity is the burst rather than the quota, so it does not start at q and count down. Treat it as room available right now.

A rejected request returns 429 RATE_LIMIT_EXCEEDED with Retry-After in seconds. Wait that long — do not retry immediately, and add jitter so a fleet of workers does not resynchronise into the next window.

Read paths fail open: if the limiter is briefly unreachable, business reads keep serving rather than going dark.

Token issuance, transcript reads and internal control-plane routes fail closed with 503 SERVICE_UNAVAILABLE. These are the paths where serving unmetered traffic is worse than serving none.

A coarse per-IP limit runs at the WAF, before traffic reaches the service, and it is not part of the quotas above: roughly 2000 requests per five minutes per source IP, and 100 per five minutes on /oauth/token. Grants authorization_code and refresh_token do not count in the WAF per-IP limit; the service limits them with the oauth-grant and oauth-ip buckets described above. GET /oauth/authorize and POST /oauth/revoke stay subject to the general per-IP limit.

It fails differently, which matters when you meet it:

  • it blocks with 403, not 429, so it reads as an authorization failure;
  • there is no error code, no Retry-After and no rate limit headers, because the request never reaches the service;
  • it counts by source IP, so every integration behind the same egress address shares it, whatever credentials they use.

A 403 with none of our error body, especially on /oauth/token, is this limit rather than a missing scope.

  • Page with limit=100 instead of many small pages — one request returns up to 100 records at the same cost as one returning 10.
  • Filter by period rather than walking the whole history repeatedly.
  • Cache the access token; every /oauth/token call consumes its own bucket.
  • Fetch transcripts only for records you will actually process. has_transcript lets you filter the list first.