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

Pagination

Lists take limit (1 to 100, default 20) and an opaque cursor from the previous page:
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 or desc), stable with an ID tiebreak. The cursor loop works for every sort. Some sort fields are computed from related data (on passes: templateName, templateVersion, revision); their cursors are offset continuations under the hood, so prefer row-field sorts for long-running exports. A cursor is only valid for the sort it was issued under; replaying it elsewhere is a 400 VALIDATION_ERROR.

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 ...From / ...To pairs of ISO 8601 timestamps, named after what they bound (createdFrom / createdTo on passes, plain from / to on scans, where the only window is effective operational scan time). 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. Numeric comparison filters take a bare number for an exact match, or a gte: / lte: prefix for a bound: revision=3 (exactly), revision=gte:3 (at least), templateVersion=lte:7 (at most).
  5. No hidden couplings: a param that only qualifies another is named after it (labelMatch qualifies label) and is rejected with 400 VALIDATION_ERROR when sent alone.
  6. Aggregates live on their own endpoints (/v1/projects/{id}/stats). Never count by requesting limit=1 lists.
  7. Unknown params are rejected with a 400 on strict endpoints, so a typo can’t silently widen your result set.

Filtering passes

GET /v1/passes supports: The canonical “door list” query returns issued passes that are valid on the selected date and have not been redeemed in its matching validity window:
validOn selects the first fixed window whose authored start date matches. A multi-day window is therefore selectable only on its start date. Because a date-only query cannot distinguish multiple windows starting on the same date, templates used by the Scanner app should define at most one; the first authored window wins for legacy policies.

Filtering scans

GET /v1/scans returns the scan ledger newest first. There is no sortBy: the order is fixed, so a cursor loop always walks the ledger in one direction. Every attempt is in the ledger, accepted and denied alike, so result=denied is your audit trail of everything turned away at the door:
scannedAt is the time these filters and the fixed newest-first ordering use. It normally records the operational time of the attempt. When an operator arms a scan-date override, scannedAt uses the selected operational date and is normalized into its matching fixed window. Exact time of day does not affect a date-selected verdict. recordedAt separately preserves when the Passlet server actually received and stored the scan.