API Reference

v1.0.0-previewOpenAPI 3.1.0

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.

Download OpenAPI document

153 endpoints across 19 groups