> ## Documentation Index
> Fetch the complete documentation index at: https://docs.passlet.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination & filtering

> The one grammar every Passlet list endpoint follows — cursors, filters, time windows, and stats.

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:

```bash theme={null}
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:

| Param                       | Meaning                                                        |
| --------------------------- | -------------------------------------------------------------- |
| `status` (repeatable)       | Any-of over pass statuses                                      |
| `externalPassId`            | Exact match on your correlation key                            |
| `search`                    | Substring across label, external ID, status                    |
| `projectId`                 | Project UUID, or the literal `null` for uncategorised passes   |
| `templateId`                | Passes issued from any version of the template                 |
| `variable` (repeatable, ≤5) | `key:value` stored-variable filter                             |
| `validOn`                   | `YYYY-MM-DD` — passes valid for scanning that day              |
| `redeemed`                  | Boolean — has (or hasn't) an unreversed accepted scan          |
| `redeemedContext`           | Scopes `redeemed` to one scan context key; requires `redeemed` |

The canonical "door list" query — issued passes valid today that haven't been redeemed today:

```bash theme={null}
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`.
