Skip to main content
Every list endpoint follows the same rules. Learn them once, use them everywhere.

Pagination

Lists take limit (1–100, default 20) and an opaque cursor from the previous page:
curl "https://api.passlet.io/v1/passes?limit=50" -H "Authorization: Bearer plt_…"
# → { "items": [...], "total": 1240, "limit": 50, "nextCursor": "…", "hasMore": true }

curl "https://api.passlet.io/v1/passes?limit=50&cursor=NEXT_CURSOR"
The envelope is always { items, total, limit, offset, nextCursor, hasMore }. Iterate until hasMore is false. (offset exists for backwards compatibility only — prefer cursors.) Sorting is sortBy (a per-endpoint enum) plus sortDir (asc | desc), stable with an ID tiebreak.

The filter grammar

  1. Different params AND together; repeating one param ORs its values. ?status=QUEUED&status=ISSUING matches either status; ?status=ISSUED&projectId=X requires both.
  2. Time windows are …Since / …Until pairs (ISO 8601 timestamps). Calendar dates are YYYY-MM-DD strings.
  3. Key-value filters use the repeatable variable=key:value micro-format, split on the first colon (values may contain colons). Repeat the same key for any-of; different keys must all match. At most 5 per request. Matching is exact after whitespace trimming.
  4. No hidden couplings — a param that only qualifies another is named after it (redeemedContext qualifies redeemed) and is rejected with 400 VALIDATION_ERROR when sent alone.
  5. Aggregates live on /stats endpoints — never count by requesting limit=1 lists.
  6. Unknown params are rejected with a 400 on strict endpoints — a typo can’t silently widen your result set.

Filtering passes

GET /v1/passes supports:
ParamMeaning
status (repeatable)Any-of over pass statuses
externalPassIdExact match on your correlation key
searchSubstring across label, external ID, status
projectIdProject UUID, or the literal null for uncategorised passes
templateIdPasses issued from any version of the template
variable (repeatable, ≤5)key:value stored-variable filter
validOnYYYY-MM-DD — passes valid for scanning that day
redeemedBoolean — has (or hasn’t) an unreversed accepted scan
redeemedContextScopes redeemed to one scan context key; requires redeemed
The canonical “door list” query — issued passes valid today that haven’t been redeemed today:
GET /v1/passes?projectId=…&status=ISSUED&validOn=2026-07-04&redeemed=false&redeemedContext=2026-07-04&sortBy=label&sortDir=asc

Filtering scans

GET /v1/scans supports projectId / passId / templateId, repeatable result (accepted | duplicate | unknown | ambiguous | denied | reversal), the shorthand resultGroup=denied (everything except accepted and reversal), scanContextKey, and the scannedSince / scannedUntil window. Scan rows carry a machine-readable reasonCode (valid, already_scanned, out_of_window, expired, revoked, quota_exhausted, …) detailing the result.

Stats instead of counting

GET /v1/scans/stats answers “how many” in one call: filter by projectId, templateId, scanContextKey (or a scanContextFrom/scanContextUntil range), and scannedSince/scannedUntil; optionally groupBy=project. Returns totalPasses, redeemed, open, totalScans, totalDenied, and lastScanAt.