The conventions every Davi API endpoint follows.
v1is in preview. The API reports its version as1.0.0-previewand is not yet frozen: the surface is still feature-incomplete, so paths and fields can still change. Build against it, but expect movement until the freeze.
Surfaces
The published API is these two base paths:
| Base path | What it is |
|---|---|
| /api/v1 | The API — everything an integration needs |
| /oauth2 | The authorization server — auth, discovery, scope metadata |
Both are served from https://api.davi.social. Paths outside them serve Davi's
own applications, change without notice, and are not reachable with third-party
credentials.
Headers
Send your access token on every request, and JSON for request bodies:
Authorization: Bearer <access_token>
Content-Type: application/json
Pagination
List endpoints are paginated. Control paging with query parameters:
| Parameter | Default | Description |
|---|---|---|
| page | 1 | 1-based page number |
| page_size | 20 | Items per page — maximum 100 |
| sort_by | — | Field to sort by (endpoint-specific) |
| sort_order | asc | asc or desc |
Every paginated response has the same envelope:
{
"total_items": 128,
"total_pages": 7,
"current_page": 1,
"items": [ /* ... */ ]
}
Errors
Errors use a consistent JSON shape:
{
"errors": { "email": "Enter a valid email address." },
"message": "Validation failed.",
"code": "validation_error"
}
errors— a map of field name to message (_rootholds form-level errors).message— a human-readable summary.code— a stable, machine-readable identifier you can branch on.
Common status 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 |
| 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 | Something went wrong on our side |
The OAuth 2.0 endpoints use the RFC 6749 error shape instead (
{ "error": "...", "error_description": "..." }).
Rate limits
Requests are rate-limited; exceeding a limit returns 429 with a Retry-After
header. Limits are grouped into buckets, keyed on the calling user + client:
| Bucket | Limit | |---|---| | Reads | 300 / minute | | Writes | 60 / minute | | Expensive writes | 20 / minute | | Unauthenticated | 30 / minute per IP, per path |
"Expensive" covers operations that do real work — check-ins, reward redemption and propagation, wallet transfers, file uploads, card claims, bulk card registration, and user search. Bulk endpoints are billed per item, so registering 100 cards costs 100 against the bucket.
The OAuth endpoints have their own tighter limits:
| Endpoint | Limit |
|---|---|
| POST /oauth2/token | 20 / minute per client + IP (failed auth: 5 / minute per IP) |
| GET /oauth2/authorize | 60 / minute |
| POST /oauth2/introspect | 100 / minute |
| POST /oauth2/revoke | 30 / minute |
Every response — not just a 429 — carries RateLimit-Limit,
RateLimit-Remaining, RateLimit-Reset, and RateLimit-Policy, so you can pace
yourself before being throttled. Cache tokens rather than requesting a new one per
call.
Compatibility
While v1 is in preview, treat these as additive and non-breaking: new response
fields, new optional parameters, new enum values, and new endpoints. Field order
is never meaningful. Write clients that ignore unknown fields.
Addressing note: most resources are addressed by slug (organizations, activities, sessions, reward templates, card models). Cards are the deliberate exception and use a UUID. A UUID is currently still accepted where a slug is expected, but that's transitional — use slugs.
Next
- Webhooks — receive events instead of polling.