Rate limits

Each key, and each signed credential, has a limit in each rate class. The reference names every operation's class as its x-rate-class.

Per key

In each class, a key may make at most this many calls a minute. The minute starts with the key's first call in that class. Past the limit, a call is answered 429 rate_limited, with Retry-After: the seconds until the minute ends.

Rate classCalls a minuteOperations
read300
  • GET /api/gap/v1/referrals
  • GET /api/gap/v1/referrals/{id}
  • GET /api/gap/v1/analytics
  • GET /api/gap/v1/program-report
  • GET /api/gap/v1/plans/enrollments
  • GET /api/gap/v1/plans/enrollments/{id}
  • GET /api/gap/v1/plans/roster-sync/sessions/{id}At launch
  • GET /api/gap/v1/plans/loss-notices
  • GET /api/gap/v1/plans/redemptions
  • GET /api/gap/v1/plans/statements
  • GET /api/gap/v1/plans/statements/{id}/lines
  • GET /api/gap/v1/eventsAt launch
  • GET /api/gap/v1/me
  • POST /api/gap/v1/review-status/lookupAt launch
  • GET /api/gap/v1/referrals/{id}/review-recordAt launch
  • GET /api/gap/v1/auditAt launch
write120
  • POST /api/gap/v1/referrals
  • PATCH /api/gap/v1/referrals/{id}
  • POST /api/gap/v1/referrals/{id}/simulateAt launch
  • POST /api/gap/v1/referrals/{id}/attestAt launch
  • POST /api/gap/v1/referrals/disclosure-preview
  • POST /api/gap/v1/plans/enrollments
  • PATCH /api/gap/v1/plans/enrollments/{id}
  • POST /api/gap/v1/plans/roster-sync/sessionsAt launch
  • POST /api/gap/v1/plans/roster-sync/sessions/{id}/chunksAt launch
  • POST /api/gap/v1/plans/roster-sync/sessions/{id}/commitAt launch
  • POST /api/gap/v1/plans/roster-sync/sessions/{id}/abortAt launch
  • POST /api/gap/v1/plans/loss-notices
  • POST /api/gap/v1/credentials/{id}/verifyAt launch
bulk12
  • POST /api/gap/v1/referrals/bulk
  • POST /api/gap/v1/plans/enrollments/bulk
  • POST /api/gap/v1/plans/roster-sync

The OpenAPI document (GET /api/gap/v1/openapi) takes no key and counts against no key's limits.

The signature checkAt launch

POST /api/gap/v1/signature-check takes no key, so it is limited by address instead: at most 60 calls a minute from one address our edge vouches for (for IPv6, from its /64). Past that, it answers 429 rate_limited, with Retry-After. Its answers carry no RateLimit header.

Failed authenticationAt launch

Past 5 failed authentications a minute from one address (for IPv6, from its /64), a call from it whose key is missing or unknown, or whose signature doesn't verify, is answered 429 auth_failures_throttled instead of 401, with Retry-After. A call with a valid key is never answered this way, nor is one from an address many callers share, such as a proxy range or a private network.

The headers

Every 429 carries Retry-After: wait that many seconds before you call again.

At launchThe responses to every key and signed credential also carry RateLimit-Policy, the limit the call counted against (its quota q and its window w in seconds), and RateLimit, what is left of it (r calls, within the next t seconds), in the form of the IETF RateLimit header fields draft. After the first read of a minute:

RateLimit headers
RateLimit-Policy: "read";q=300;w=60
RateLimit: "read";r=299;t=60

Backing off

  • On a 429, wait the seconds in Retry-After, then send the same request again. Send a write with the same Idempotency-Key, so nothing is created twice.
  • To send many referrals or roster changes, use the bulk operations: a bulk call counts once, however many rows it holds.
  • To follow your referrals, page through GET /api/gap/v1/referrals with updated_after rather than reading each referral in turn: Pagination says how.