API Reference
The Onsend public REST API (v1) — read-only leaderboard, user standing, and stats endpoints with API-key authentication.
Onsend's external, client-facing REST API. Read-only. All endpoints are
GET and return JSON.
The API is versioned at /api/v1/. Future shape changes will ship under
/api/v2, so a v1 integration will keep working unchanged.
Base URL
Your tenant is determined by your API key (see Authentication), not by the host, so you do not need a tenant-specific URL. Use the apex base URL above for every request.
Your tenant dashboard subdomain works identically if you prefer it
(https://<your-tenant>.onsend.xyz/api/v1), since the key still selects
the tenant either way.
Quick start
Authentication
Every request requires an API key. A tenant admin mints one under
Settings > API Keys (/admin/settings/api-keys). The full key
(onsend_...) is shown only once at creation. Store it securely; Onsend
keeps only a hash and cannot show it again.
Pass the key in either header:
or
If both are present, Authorization: Bearer is used.
A missing, invalid, revoked, or expired key returns 401.
Tenant scoping
Your key is bound to exactly one tenant. Every response contains only
that tenant's data. There is no parameter that can read another tenant's
data; the optional campaignId parameter is checked against your tenant
and returns 404 if it does not belong to you.
Scopes
API keys carry a scopes list, but v1 does not enforce per-scope
access: any valid (non-revoked, non-expired) key for your tenant can
call all three v1 endpoints. See Future enhancements.
Rate limiting
Each API key is limited to 60 requests per minute.
When you exceed the limit you receive 429 with a Retry-After header
giving the number of seconds to wait before retrying:
Respect Retry-After (or retryAfterSeconds) and retry after that
delay.
Response envelope
Successful responses wrap the payload in data:
Errors use a consistent shape:
Validation errors (400) additionally include a details object
describing the offending parameters. The 429 error additionally
includes retryAfterSeconds.
Status codes and error codes
| HTTP | error.code | Meaning |
|---|---|---|
| 200 | (none) | Success. |
| 400 | invalid_params | A query parameter is missing, malformed, or out of range. |
| 401 | unauthorized | Missing, invalid, revoked, or expired API key. |
| 404 | not_found | Unknown user, or a campaignId that is not in your tenant. |
| 429 | rate_limited | Over the 60/minute per-key budget. See Retry-After. |
| 500 | internal_error | Unexpected server error. |
Example error bodies:
Privacy
Responses never contain email addresses or any other personal data. The
only public identifiers are the wallet address (which may be null for
participants who joined with email only) and the user-chosen display
name (which may also be null).
GET /api/v1/leaderboard
Your tenant's ranked leaderboard.
Query parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
limit | integer | no | 50 | Page size. Range 1 to 100. Out of range returns 400. |
cursor | string | no | (none) | Opaque pagination cursor from a previous response's nextCursor. |
window | enum | no | all | Timeframe: all, 7d, or 30d (tenant-wide). |
campaignId | string | no | (none) | Scope to one campaign's leaderboard. Only valid with window=all. |
Notes:
window=allranks participants by their tenant-wide total points and is always up to date.window=7dandwindow=30dare computed by a daily job; their entries are empty until that job has run for the window.campaignIdreturns that campaign's all-time leaderboard. CombiningcampaignIdwithwindow=7dorwindow=30dreturns400.
Response
| Field | Type | Description |
|---|---|---|
entries[] | array | The page of ranked entries. |
entries[].rank | integer | 1-based rank within the selected window. |
entries[].wallet | string | null | Wallet address, or null for email-only users. |
entries[].displayName | string | null | Chosen handle, or null. |
entries[].avatarUrl | string | null | Avatar URL, or null. |
entries[].points | integer | Points for the selected window. |
total | integer | Total ranked participants for the selection. |
nextCursor | string | null | Pass back as cursor for the next page. null when there are no more pages. |
Pagination
Request the first page, then keep passing the returned nextCursor back
as ?cursor= until nextCursor is null:
GET /api/v1/users/{identifier}
A single user's standing. {identifier} is the user's wallet address
(case-insensitive). Returns 404 if no such wallet participates in your
tenant.
Query parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
window | enum | no | all | Timeframe: all, 7d, or 30d. |
campaignId | string | no | (none) | Standing within one campaign. Only valid with window=all. |
Response
| Field | Type | Description |
|---|---|---|
wallet | string | null | The user's wallet address. |
displayName | string | null | Chosen handle, or null. |
avatarUrl | string | null | Avatar URL, or null. |
rank | integer | null | Rank within the window. null when the user exists but has no activity in a 7d/30d window. |
points | integer | Points for the selected window. |
window | string | Echoes the resolved window (all, 7d, or 30d). |
campaignId | string | Present only when the request was scoped to a campaign. |
Example:
GET /api/v1/stats
Aggregate, privacy-safe stats for your tenant.
Response
| Field | Type | Description |
|---|---|---|
totalParticipants | integer | Distinct participants in the tenant. |
totalCampaigns | integer | Campaigns in the tenant. |
totalPointsDistributed | integer | Sum of all participants' points. |
totalQuestsCompleted | integer | Verified quest completions in the tenant. |
Future enhancements
- Per-scope gating. Keys carry a
scopeslist that v1 does not enforce. A future version may require specific scopes (for exampleread:leaderboard) per endpoint. v1 keys will keep working.