Request conventions are described in Making Requests.
Pagination
| Parameter | Default | Description |
|---|---|---|
page | 1 | 1-based page number |
page_size | 20 | Items per page. Maximum 100 |
sort_by | none | Field to sort by. Endpoint-specific |
sort_order | asc | asc or desc |
total_items counts the records paged over, not the items returned. An endpoint
that filters after paging can return a short or empty page while later pages
still hold items. Continue while current_page < total_pages.
Status and error codes
| Status | code | Meaning |
|---|---|---|
400 | bad_request | Malformed request |
401 | invalid_credentials | Missing or invalid token |
403 | forbidden | Authenticated, but not allowed: missing scope or role |
404 | not_found | Resource doesn't exist, or isn't yours |
409 | conflict | Conflicts with current state |
422 | validation_error | Request body failed validation |
429 | rate_limit_exceeded | Rate limit exceeded. See Retry-After |
500 | internal_error | Error on Davi's side |
A 404 for a record that does not exist and a 404 for a record not visible to
the caller are indistinguishable.
The OAuth 2.0 endpoints use the RFC 6749 error shape instead:
{ "error": "...", "error_description": "..." }.
errors is a map of context to value. For validation failures the keys are
field names and the values are messages; for other refusals a key does not
necessarily name an input.
Specific codes
These codes are narrower than their status. Several share a status and need different handling.
code | Status | Raised when |
|---|---|---|
identifier_unresolved | 404 | The supplied identifier matched no wallet or user |
membership_required | 403 | The action needs an active membership, or one at a specific tier |
idempotency_conflict | 409 | An idempotency key was reused for a different subject |
forbidden_not_authorized | 403 | Acting on somebody else without the authority to do so |
membership_required means the user must join or change tier.
forbidden_not_authorized means the caller is the wrong actor.
POST /sessions/{slug}/admit does not use error statuses for refusals at the
door. Every gate that turns somebody away, including closed doors and hours,
answers 200 with decision: "blocked" and a blocked_reason. Only an
unresolvable identifier, a caller without the authority, and a reused
idempotency key return error statuses. See
Org Activities.
Rate limits
Requests are counted over a sliding window: an allowance refills continuously rather than resetting on the minute. Authenticated buckets are keyed on user and client together, so traffic from another application the same user has authorized does not count against yours.
| Bucket | Limit |
|---|---|
| Reads | 300 / minute |
| Writes | 60 / minute |
| Expensive writes | 20 / minute |
| Unauthenticated | 30 / minute, per IP and per path |
Expensive writes
The expensive bucket is a fixed list of routes:
| Route | Charged | Reason |
|---|---|---|
POST /sessions/{session_slug}/admit | per call | Records attendance at a door |
POST /rewards/{reward_slug}/claim | per call | Ledger write |
POST /rewards/{reward_slug}/redeem | per call | Ledger write |
POST /rewards/{reward_slug}/propagate | per call | Ledger write |
POST /rewards/{reward_slug}/trigger | per call | Ledger write |
POST /organizations/{organization_slug}/cards | per item | Takes cards into inventory, up to 20 per call |
POST /activities/{activity_slug}/sessions/batch | per call | Creates a run of sessions in one statement |
POST /organizations/{organization_slug}/cards is the only endpoint charged per
item: twenty cards in one request spend twenty of the expensive allowance. The
batch cap and the bucket are both 20, so one full batch uses the whole minute's
allowance. A session batch is one charge however many dates it creates.
Per-endpoint overrides
These endpoints have their own limits in addition to the buckets. Where both apply, the tighter one decides.
| Endpoint | Limit | Keyed on |
|---|---|---|
POST /oauth2/token | 20 / minute | client_id + IP |
POST /oauth2/token (failed auth) | 5 / minute | IP |
GET /oauth2/authorize | 60 / minute | User session |
POST /oauth2/introspect | 100 / minute | client_id |
POST /oauth2/revoke | 30 / minute | client_id |
| Wallet transaction signing | 100 / hour | Wallet |
| Account lookup | 15 / minute | none |
Headers
Every response carries these headers, not only a 429:
| Header | Meaning |
|---|---|
RateLimit-Limit | The bucket's allowance |
RateLimit-Remaining | What is left in the current window |
RateLimit-Reset | Seconds until the window refills |
RateLimit-Policy | The policy, e.g. 300;w=60 |
Retry-After | On a 429 only. Seconds to wait |
The same values are also sent under the older X-RateLimit-* names.
A 429 is refused before the request runs. Retrying after Retry-After needs
no reconciliation of state.
Caching an access token for its lifetime, rather than requesting one per call, is the largest saving on the token endpoint's allowance.
Next
- Making Requests: the conventions behind these values.
- Scopes: what a
403for a missing scope means.