Skip to content

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).

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.net

All 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:

SchemeHeaderActs asEndpoints
OAuth access tokenAuthorization: Bearer <JWT>A signed-in user/api/v1/*
Organization API keyAuthorization: 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).

APILimit
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:

ParameterNotes
limitPage size, 1–100. Default 30.
cursorThe previous page’s next_cursor. Omit for the first page.
searchCase-insensitive match on guest name / email / phone.

Response envelope:

FieldMeaning
successtrue.
itemsThe page of results.
totalTotal count. Unreliable (may be null) while search is set.
next_cursorOpaque cursor for the next page (null on the last page).
has_moreWhether another page exists.
usageMonthly 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.