API Reference
Davi API
The Davi API covers organizations, memberships, activities, rewards, cards, wallets and contacts.
Surfaces
| Base path | What it is |
|---|---|
/api/v1 |
The API. Everything an integration needs. |
/oauth2 |
The authorization server — authentication, discovery, scope metadata. |
Paths outside these two are not part of the published API. They serve Davi's own applications, change without notice, and are not reachable with third-party credentials.
Authentication
Every request carries an OAuth2 bearer token:
Authorization: Bearer <access_token>
The authorization server is at /oauth2 and describes itself at
/oauth2/.well-known/openid-configuration, including the full list of scopes
it will issue. Three grants are available:
authorization_code(with PKCE, required for every client) — acting on behalf of a user.client_credentials— acting as your own application, with no user.urn:ietf:params:oauth:grant-type:token-exchange— exchanging a user token for one scoped to a specific organization. The subject token must be one issued to your own client.
/oauth2/scopes returns human-readable descriptions for a set of scope names,
and /oauth2/client-info returns an application's name and icon. Both are
unauthenticated, so a consent screen can render before anyone has signed in.
Token lifetime
Access tokens are short-lived. Treat expires_in on the token response as
authoritative rather than assuming a duration.
Refresh tokens rotate. Every refresh returns a new refresh token and retires the one you sent. Two consequences:
- Store the new refresh token from every refresh response. Keeping the original will log the user out at its next use.
- Presenting an already-used refresh token revokes the entire session, not just that token. A replayed refresh token is treated as a stolen one. If your client can refresh from more than one place at once, serialize it — concurrent refreshes race, and the loser's token is a replay.
Revoking a token through /oauth2/revoke ends the session it belongs to.
Scopes
A scope answers "what did the user let this application do?". That is a different question from "what may this user do?", which is decided by their role in the organization that owns the resource.
Both have to allow an action. A token missing a scope is refused even when the user could perform the action themselves, and holding a scope grants nothing on a resource the user has no role in. Request every scope your integration needs at authorization time; a scope you did not ask for is not one you can fall back on later.
Pagination
List endpoints take page (from 1) and page_size (up to 100), and return:
{
"total_items": 137,
"total_pages": 7,
"current_page": 1,
"items": []
}
Errors
Errors carry a machine-readable code, a human-readable message, and an
errors map keyed by the field at fault — _root when the failure is not
tied to one field:
{
"code": "validation_error",
"message": "Invalid email",
"errors": { "email": "Invalid email" }
}
Status codes follow the usual meanings: 400 malformed, 401 unauthenticated,
403 authenticated but not permitted, 404 absent or not yours, 409
conflicts with existing state, 422 failed validation.
Rate limits
Every request is charged to one of four buckets. Requests are counted over a sliding window, so an allowance refills continuously rather than resetting on the minute.
| Bucket | Limit | What falls in it |
|---|---|---|
read |
300/minute | GET, HEAD, OPTIONS |
write |
60/minute | Every other method |
expensive |
20/minute | Ledger writes, outbound calls, search, bulk assign, claim |
unauthenticated |
30/minute | Requests with no token, counted per IP and per path |
The authenticated buckets are keyed on user and application together, so your integration's traffic never competes with another application the same user has authorized.
A few endpoints carry their own tighter limits — the OAuth2 token endpoint and wallet transaction signing among them. Where both apply, the tighter one decides.
Bulk endpoints are charged per item rather than per call: assigning 50 cards in
one request spends 50 of the expensive allowance.
Every response states where you stand, so a client can pace itself without having to hit the limit first:
RateLimit-Limit: 300
RateLimit-Remaining: 297
RateLimit-Reset: 60
RateLimit-Policy: 300;w=60
The same values are also sent under the older X-RateLimit-* names.
Exceeding a limit returns 429 with code: "rate_limit_exceeded" in the
standard error envelope, and a Retry-After header in seconds. Wait for it
rather than retrying immediately.
Retries
There is no general Idempotency-Key header. Treat a write whose response
you did not receive as having an unknown outcome: re-read the resource rather
than repeating the write, unless an endpoint documents itself as safe to
repeat. Some operations deduplicate server-side — attendance check-in and
wallet transfers among them — but that is a property of those endpoints, not a
rule that holds across the API.
A 429 is not a failed write — the request was refused before it ran. Retry it
after the delay in Retry-After.
Stability
v1 is a preview. The path carries a version, but no stability guarantee
has been made for it yet: the surface is still being completed, and a breaking
change may still land in v1 until one is. Once the guarantee is made, breaking
changes go to a new version instead.
Build against it, but expect to be told about a change rather than to discover it. Known consumers are notified before a breaking change ships.
Either way, these are not breaking changes and a client has to tolerate them:
- New fields appear in responses. Ignore what you do not recognize rather than rejecting the payload.
- New optional request fields, query parameters and enum values appear. Treat an unfamiliar enum value as unknown rather than as an error.
- New endpoints appear, including new methods on paths you already use.
- Field order is not meaningful anywhere, including in list results without an explicit sort.
153 endpoints across 19 groups