Conventions
Copy page
How the EntryBit REST API works across every endpoint: base URL, the two auth schemes, error envelopes, rate limits, and keyset pagination (limit / cursor / next_cursor / has_more / search).
Updated
Conventions that hold across every endpoint of the resource API. Read this once and the individual endpoint pages (Passes, Invitations, Organization) are just field lists.
Base URL
https://entrybit.netAll traffic is JSON over HTTPS (TLS required). Credential-bearing responses set Cache-Control: no-store. Never place a credential in a URL.
Authentication
Two schemes, depending on who is acting — see Authentication for the full picture:
| Scheme | Header | Acts as | Endpoints |
|---|---|---|---|
| OAuth access token | Authorization: Bearer <JWT> | A signed-in user | /api/v1/* |
| Organization API key | Authorization: Bearer eb_sk_… (or X-API-Key) | The organization | /api/v1/org/* |
An access token is a short-lived RS256 JWT from the token endpoint; an API key is a long-lived secret from Settings → API keys.
Error envelopes
EntryBit uses different error shapes for protocol vs business failures. In all of them, branch on the code, never on the human-readable text — descriptions are deliberately generic.
Bearer challenges (RFC 6750) — auth failures on any resource endpoint. A WWW-Authenticate: Bearer header accompanies the body:
{ "error": "insufficient_scope" }The error is invalid_token (401) or insufficient_scope (403).
Business errors (AppError) — validation, not-found, and conflict failures come in one of two equivalent shapes:
{ "success": false, "error": "Facility not found" }{ "code": "…", "message": "Facility not found" }Either form carries a machine-readable field (error or code) and a human-readable message; the exact code value depends on the endpoint.
Quota (402) — exhausting the monthly invite allowance returns a distinct shape:
{
"code": "CAPACITY_EXCEEDED",
"message": "Monthly guest-invite allowance exceeded",
"dimension": "guest_invites",
"limit": 50,
"current": 50,
"requested": 3
}The full mapping of status codes to reactions is in Errors.
Rate limits
Per-credential and per-IP buckets; exceeding one returns 429 with { "error": "temporarily_unavailable" }. Back off and retry (exponential backoff with jitter).
| API | Limit |
|---|---|
User-delegated (/api/v1/*) | 120 requests / minute / user |
Organization (/api/v1/org/*) | 300 requests / minute / source IP |
Keyset pagination
List endpoints are keyset (cursor) paginated, newest first — stable and efficient even as rows are added underneath you.
Query parameters:
| Parameter | Notes |
|---|---|
limit | Page size, 1–100. Default 30. |
cursor | The previous page’s next_cursor. Omit for the first page. |
search | Case-insensitive match on guest name / email / phone. |
Response envelope:
| Field | Meaning |
|---|---|
success | true. |
items | The page of results. |
total | Total count. Unreliable (may be null) while search is set. |
next_cursor | Opaque cursor for the next page (null on the last page). |
has_more | Whether another page exists. |
usage | Monthly allowance block — first page of a user-delegated pass listing only. |
Loop until has_more is false:
# First page
curl "https://entrybit.net/api/v1/passes?limit=50" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Next page — feed next_cursor back as cursor
curl "https://entrybit.net/api/v1/passes?limit=50&cursor=eyJpZCI6…" \
-H "Authorization: Bearer $ACCESS_TOKEN"Or as a lazy async generator — api() is the authenticated fetch wrapper from Errors:
type Page<T> = {
success: true;
items: T[];
total: number | null;
next_cursor: string | null;
has_more: boolean;
};
/** Yields every pass, fetching pages only as they're consumed. */
async function* eachPass<T>(search?: string): AsyncGenerator<T> {
let cursor: string | undefined;
do {
const qs = new URLSearchParams({ limit: '100' });
if (search) qs.set('search', search);
if (cursor) qs.set('cursor', cursor); // opaque — pass back verbatim
const page = await api<Page<T>>(`/api/v1/passes?${qs}`);
yield* page.items;
cursor = page.has_more && page.next_cursor ? page.next_cursor : undefined;
} while (cursor);
}
for await (const pass of eachPass('dana')) {
// …
}Treat next_cursor as opaque — don’t parse or construct it. When has_more is false, you’ve read every row.
Identifiers
Public resources use prefixed, opaque public ids — for example a pass is gst_9f1c2ab34cd56ef7. Use these in URLs and store them as strings; the numeric id some responses also include is an internal, backward-compatibility field.