Skip to main content
Redemption is where passes meet the real world: a barcode is scanned, Passlet decides accept or deny based on the template’s redemption rule, and the decision is recorded.

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.
Without an explicit rule, a pass can be redeemed once over its lifetime. To allow one redemption on each event day, define a fixed window for each day with a per-window limit of one, or use a recurring daily limit with the event’s timezone.

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.voided webhook fires, in addition to the usual pass.scanned event, so your systems learn about it the same way as for a manual void.
Things to know before turning it on:
  • 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.
Scanner devices authenticate with scoped credentials: they never sign in to the admin console and can access only scanner operations and their assigned project roster. Disconnecting a device from within the Scanner app is blocked while it still holds unresolved scan reports or accepted roster-confirmation shadows, preventing loss of the audit queue and stale local admission state.

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_window instead of the misleading no_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 unverifiable rather 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, marked verified: 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: accepted with metadata.conflict: "quota_exhausted".
  • The device denied a pass that was issued after its roster snapshot: result: denied, reason: no_match, with metadata.conflict: "valid" (and the scan is still linked to the pass the server resolved).
Conflicts are not surfaced in the Scanner app. By the time the device reconnects the moment has passed, so they exist for after-the-fact analysis: filter the ledger for 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.
Side effects still apply on delivery: an offline-accepted scan consumes real capacity, so a lifetime limit with auto-void voids the pass when the queued scan that spent the final slot arrives (“offline scans void on reconnect”). Disabling the flag later stops new roster downloads immediately and clears the device’s cached roster on its next contact; scans the device already decided while offline are still accepted on delivery, since they record scans that actually happened.

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 to scans:read, then:
  • Pull: GET /v1/scans lists 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.scanned webhook, which fires for every recorded decision (accepted and denied, online and offline). See Webhooks.
A CRM sync can treat the ledger as an append-only event stream: one verdict per scan, no later mutations, and 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.scanned event fires for every recorded scan decision. See Webhooks.