Pagination
Lists takelimit (1 to 100, default 20) and an opaque cursor from the previous page:
{ 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
- Different params AND together; repeating one param ORs its values.
?status=QUEUED&status=ISSUINGmatches either status;?status=ISSUED&projectId=Xrequires both. - Time windows are
...From/...Topairs of ISO 8601 timestamps, named after what they bound (createdFrom/createdToon passes, plainfrom/toon scans, where the only window is effective operational scan time). Calendar dates areYYYY-MM-DDstrings. - Key-value filters use the repeatable
variable=key:valuemicro-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. - 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). - No hidden couplings: a param that only qualifies another is named after it
(
labelMatchqualifieslabel) and is rejected with400 VALIDATION_ERRORwhen sent alone. - Aggregates live on their own endpoints (
/v1/projects/{id}/stats). Never count by requestinglimit=1lists. - 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.