Redemption rules
Each template carries one redemption rule: the template-wide policy for when and how many times a pass can be scanned. Configure it in the template editor.When can the pass be scanned?
- Always valid: no time restriction.
- During date windows: one or more windows with open, fixed, issue-relative, or variable-driven bounds. Each window can cap its own redemptions, and a multiple-choice variable can select which windows apply per pass (for example, which festival days a ticket covers). A fixed start is included and its end is the first excluded instant, so two windows may touch without sharing a scan.
- After first scan: an activation flow. The pass becomes valid for N hours after its first scan (day tickets, time-boxed vouchers).
How often can it be scanned?
- Recurring limit: for example once per day, in a timezone you pick.
- Cooldown: a minimum number of minutes between scans.
- Lifetime total: a maximum number of scans over the pass’s life (punch cards). Optionally auto-voids the pass when the limit is reached.
Auto-void after reaching limit
When a rule has a Lifetime total, you can also turn on Auto-void after reaching limit in the template editor. With the toggle on, a pass is voided automatically the moment its last allowed scan is used, with no manual cleanup needed. This is built for punch cards, multi-entry tickets, and vouchers that should stop working entirely once they are used up. Here is exactly what happens on the final scan:- The final scan is accepted normally. The guest gets in, and the operator sees the usual green confirmation plus a short note that the pass has now been voided.
- The pass is voided immediately after that scan. This is the same operation as Void pass on the pass list: the pass links stop working and the pass is flagged for removal from Apple and Google wallets (removal can take up to 24 hours to propagate on the device).
- Every later scan attempt is rejected. The pass’s scan codes are revoked together with the pass, so re-scanning the wallet barcode reads Not recognized, and a manual check-in of the pass is denied as Voided. This matches what happens after a manual void.
- A
pass.voidedwebhook fires, in addition to the usualpass.scannedevent, so your systems learn about it the same way as for a manual void.
- Only the Lifetime total triggers auto-void. Recurring limits, cooldowns, and per-window caps deny further scans when exhausted, but they never void the pass.
- Voiding is permanent. If guests might need re-entry after an accidental final scan, prefer a plain lifetime total without auto-void.
- The rule is stamped onto each pass when it is issued. Turning the toggle on or off later affects newly issued passes only; already-issued passes keep the rule they were issued with until you update them or upgrade them to the new template version.
- Offline scans void on reconnect. An enrolled Scanner app can accept the final scan while offline; the pass is then voided when the device reconnects and the queued scan syncs.
Validity windows are projected into the wallet’s native relevance fields (Apple
relevantDates,
the start of Google’s validTimeInterval) so passes surface at the right times on the device.
Windows never affect pass expiry: a pass expires only when the template’s Expires at setting
says so. Online scans normally evaluate time-based rules against the live server clock. The
Scanner app always sends its selected scan date. For a pass with fixed windows, that date selects
the first window whose authored start date matches; the current time of day does not accept or
deny the scan. The effective scannedAt is normalized into that window so history and usage stay
assigned to it. If no window starts on the selected date, the scan is denied as out_of_window. A
Scanner device with offline scanning enabled applies the same rule locally.Scan outcomes
Every scan gets a binary result,accepted or denied, plus a machine-readable reason explaining it (for example already_used, out_of_window, expired, revoked, quota_exhausted). A multiple_matches denial returns candidate passes for the operator to confirm.
The Scanner app presents out_of_window as an explicit red denial, so an earlier acceptance or
spent quota never hides the fact that the current attempt is outside every applicable window.
The recorded result is denied and the machine-readable reason is out_of_window.
When the Scanner app shows passes for a selected date, Scanned is scoped to the matching
redemption window. An acceptance in one window does not mark the same pass as scanned in a later,
disjoint window whose start is on another date. A multi-day window is selectable only on its
authored start date, not on every date it spans. The date picker does not distinguish two windows
that start on the same date, so templates used by the Scanner app should define at most one fixed
window per start date; if legacy data contains more, the first authored window wins. Passes without
fixed windows, including the default single-use rule, keep lifetime-wide redemption state. API
requests that omit validOn also use lifetime-wide redeemed state.
Every scan has two timestamps. scannedAt is the effective operational scan time used in scan
history, ordering, and from/to filters. It is normally the time of the scan; when the scan-date
override is active, it is normalized to the selected date and, for a fixed-window pass, into the
selected window. recordedAt is the real time the Passlet server received and stored the
attempt. These values can therefore differ for an override or for a scan delivered after an offline
period.
Each scan also carries verified: online when the Passlet server made the decision in-band, offline when a Scanner device decided locally and delivered the scan after reconnecting. See Offline scanning for exactly what that means.
The Scanner app
The Scanner app page in the sidebar manages the mobile devices that scan passes:1
Connect a device
Show the setup QR to the device running the Passlet Scanner app. The device enrolls against
your workspace and appears in the Connected devices table.
2
Operate
Operators can scope scanning to a project, select the scan date, scan wallet barcodes, and get
an instant accept or deny verdict with the reason. Devices report their name, platform, and
last-seen time. With offline scanning enabled, the device also keeps
working through connectivity drops.
3
Revoke when needed
Revoke device stops new authenticated requests and roster syncs immediately; it can be
reconnected later with the setup QR. Rotate the setup QR if it leaked.
Offline scanning
Offline scanning is off by default and enabled per device on the Scanner app page. It is a deliberate trade: scanning keeps working without connectivity, in exchange for decisions that are made from a device-local snapshot instead of the live server. Read this section before enabling it.How it works
- With the flag on, the device downloads one project roster snapshot (pass identity, status,
redemption rule, and redemption history projections) plus the current signed-token verification
keys. It includes every issued identity in that project so an offline scan outside the selected
date can return
out_of_windowinstead of the misleadingno_match; Home and Passes still display only passes eligible for their selected date. The browse date and camera scan date remain independent UI selections, while both reuse the same complete project roster. The server must explicitly confirm project-roster mode before the app caches the response. A roster stays usable for 48 hours; after that, offline scanning fails closed until the device syncs again. - While disconnected, the device verifies the wallet barcode’s signature locally, looks the pass
up in its roster, and applies the same redemption gates the server would (status, windows,
activation, cooldown, recurring and lifetime limits), counting its own queued accepted scans so
the same pass cannot be accepted twice on one device. When the snapshot cannot answer a gate safely,
the device denies with reason
unverifiablerather than guessing. - Every offline decision is stored durably on the device and delivered to the server when connectivity returns.
- An accepted online scan remains as an already-delivered local shadow until a fresh roster has observed it. This closes the short online-to-offline transition where an older cache could otherwise admit the same quota again. An accepted bulk scan instead invalidates the identity cache before returning and keeps offline admission unavailable until refresh.
Offline decisions are final
The scan ledger records the decision that was acted on, so an offline-decided scan is stored exactly as the device decided it, markedverified: offline. The server never replays or
overwrites it.
When the delivered verdict disagrees with what the server itself would have decided at delivery
time, the server stamps the scan’s metadata with a conflict key whose value is the reason
the server would have returned. Two examples:
- The device accepted a pass that another device had already accepted offline:
result: acceptedwithmetadata.conflict: "quota_exhausted". - The device denied a pass that was issued after its roster snapshot:
result: denied,reason: no_match, withmetadata.conflict: "valid"(and the scan is still linked to the pass the server resolved).
verified: offline scans and inspect metadata.conflict (via GET /v1/scans or your webhook
consumer) to quantify double acceptances or wrongly denied passes.
What offline mode cannot prevent
- Cross-device double acceptance. Two offline devices each only know their own queue, so both
can accept the same pass. The ledger will show both accepted scans, one of them stamped with
conflict, and a pass can exceed its limits this way. This is inherent to offline operation: both scans really happened. - Stale rosters. Passes issued or voided after the snapshot are unknown to the device until the next sync.
Reading scans from your systems
Scanning is read-only on the public API: passes are scanned with the Passlet Scanner app or checked in from the admin console, and your integration observes the results. Give the integration a token scoped toscans:read, then:
- Pull:
GET /v1/scanslists the ledger newest first, with filters for pass, project, result, reason, and effective operational-time window;GET /v1/scans/{id}fetches one scan. - Push: subscribe to the
pass.scannedwebhook, which fires for every recorded decision (accepted and denied, online and offline). See Webhooks.
verified plus metadata.conflict to explain offline edge cases.
Where scans show up
- Pass detail: per-pass Scan history and Audit history tabs.
- Projects: per-project scan panels and stats.
- API:
GET /v1/scans. - Webhooks: a
pass.scannedevent fires for every recorded scan decision. See Webhooks.