Conventions
העתקת עמוד
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).
עודכן
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. Prefer reading the response headers over hardcoding a backoff — every 429 tells you exactly when to come back.
| API | Limit |
|---|---|
User-delegated (/api/v1/*) | 120 requests / minute / user |
Organization (/api/v1/org/*) | 300 requests / minute / source IP |
Every 429 response carries these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before retrying — honor this one above all. |
RateLimit | Current IETF draft (structured field): "default";r=0;t=100 — remaining r, reset-in t seconds. |
RateLimit-Policy | The policy (structured field): "default";q=120;w=60 — quota q per window w seconds. |
RateLimit-Limit | Older-draft form of the request cap (kept for deployed parsers). |
RateLimit-Remaining | Older-draft form; always 0 on a 429. |
RateLimit-Reset | Older-draft form; seconds (delta) until the window resets. |
X-RateLimit-Reset | Pre-standard legacy: same reset as an epoch timestamp. |
The IETF rate-limit header work (draft-ietf-httpapi-ratelimit-headers) has evolved through incompatible shapes and is still pre-RFC, so we deliberately emit all three generations — parse whichever your library supports, preferring RateLimit/RateLimit-Policy. The JSON body also repeats retry_after (seconds) for clients that only parse bodies. Sustained abuse escalates the wait (lockouts of 15 min → 1 h → 24 h), so a client that ignores Retry-After locks itself out for longer — respect the header, add jitter, and never retry in a tight loop.
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 — guest name / email / phone on pass lists, member name (plus email/phone on the contact tier) on the member directory. |
fields | Member endpoints only: Graph-style sparse fieldsets — see Organization. |
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.

