Troubleshooting: wallet pass generation counter and void conflict
Three symptoms send operators here. A holder’s phone shows stale pass content after your update succeeded. An update or void returns409 CONFLICT. Or a
deep-link that “top up points” flow minted yesterday opens the wrong pass. Each
maps to one of the lifecycle edges — active, voided, expired — the
wallet pass lifecycle defines; this page
names the check for each edge and the fix at the level you own.
For the issuance workflow itself and the field reference, start at the
Wallet Passes channel guide.
The wallet-pass operations you run — issue, update, void — are tenant-owned
controls that move through your own ledger. A 409 or a drifted generation is
the platform honoring the transition you already made, not a platform fault.
The fixes below are yours to run; escalations go to the ledger read-path, not
the wallet.
Cause table
Read the pass first withGET /wallet-passes/:id, or list with the filter the
channel guide documents (?type=…&status=…&limit=…). The returned status
and generation decide which row applies.
The edges are the whole diagnosis:
active— accepts update and void. Each accepted update bumpsgenerationby exactly one.voided— terminal. Update and re-void return409 CONFLICT; issue a replacement.expired— the wallet platform renders the pass expired when itsexpires_atlapses, but the ledgerstatusstill readsactiveuntil you void. An expired-but-active pass accepts update, so restoring validity means patching a laterexpires_at; the void decision is separate.
Why the generation counter drifts
generation counts accepted update events, starting at 0 on issue. Drift
is always a stale-read artifact, never a counter bug. When two issuers work on
the same contact — one issuing, one polling — a poller that holds a read from
before the latest update reports a lower number than the issuer’s sum. Because
reads fold the pass’s full event history at request time, a fresh GET always
returns the counter’s exact current value; a cached copy does not.
Recovery is the same either way: re-issue once under the same
idempotency_key (the replay is folded to current state — never stale) and
regenerate the holder’s save link from the fresh GET. The holder refreshes
their copy on the next open of the new link.
409 CONFLICT on update-after-void vs duplicate-issue idempotency
Two distinct edges produce a 409, and they must not be conflated.
- Update or void after void. The pass already transitioned to
voided. The controller rejects the transition with409, names the current status, and returns areasonlikealready_voided. That edge is terminal — the pass can never be revived, so the recovery is to issue a new pass. - Duplicate issue.
POST /wallet-passes/issueis idempotent: send the sameidempotency_key(or theIdempotency-Keyheader) and the request replays the first issued pass instead of minting a duplicate, returning200withreplayed: truerather than a conflicting 409. The key lookup and the issue append run inside one transaction, so a double-submit or a client retry can never create two passes.
active accepts, voided
rejects) rather than against blanket 409 handling — void’s 409 is the correct
answer there; the issue endpoint’s success-based replay is the correct answer
for a duplicated enrollment.
Generation is not sequence — the highest observed wins
When two clients issue and then independently update the same pass, a reader that got a snapshot between the issuer’s events might seegeneration: 3 and
then, on a later read, generation: 5 on the same pass. That jump is expected:
each accepted update increments by exactly one, and serial issuances across
clients mean the counter’s total can only move forward. Never order events
by generation as though it were a sequence position — the highest value a
reader has observed is the canonical count of accepted updates, and that is
the value the wallet uses to decide that the holder needs a refresh.
When the issued link still opens the old pass
The Google save link and the Apple.pkpass transport path encode the pass
content at mint time. An update after minting leaves the previously minted
link resolving to the pre-update snapshot, and a holder’s wallet will not
re-fetch until it opens a fresh link. Recovery:
- Re-
GETthe pass — the response rebuilds the platform payloads fresh on every read, so the currentplatforms.google.save_url(or the Apple serial) now points at the latest content. - Cache-bust the link hand-off: route the new save URL through your link shortener with a fresh slug, or attach a query marker your redirect resolves, so a cached redirect or an analytics page never serves the stale URL.
- Re-issued passes (the idempotent-replay path) replay the current pass, so a re-issue under the same key is itself cache-busting — your holders pull the newest content when they open the refreshed link.
What NOT to do
- Do not use a second, new pass to fix drift. A re-issue with a new
idempotency_keymints a distinct pass; the old one still exists. Recovery is always the same key → same pass → newest content. - Do not retry a 409 void/update with backoff. The pass is
voidedon the platform side; the retry can never succeed and it delays issuing the replacement. - Do not diff
generationacross clients. Treat it only as “something moved” on a single pass; compare it across polls, not across issuers. - Do not assume a still-
activeexpired pass is safe to void-auto. The pass’s ledger status remainsactivepastexpires_at; if you want the audit to readvoided, call the void yourself.
When to escalate
Escalate to support@devotel.io only when the ledger-fold read itself misbehaves:- A fresh
GET /wallet-passes/:idreturns agenerationlower than the count of accepted updates your client has logged (that would break the fold, and only the platform can re-fold). POST /wallet-passes/:id/updateorvoidreturns409on a pass whoseGETstill returnsactive— divergence between the transition guard and the read projection.- An idempotent issue returns
replayed: truebut the replayed pass shows issued-time content rather than the current lifecycle fold.
request_id from meta.request_id, the wps_ pass id, the
status and generation you observed, and the idempotency_key you used.
Related
- Wallet Passes channel guide — issuance workflow, the platform-payload builder, and the common error table.
- Wallet pass lifecycle concept — the state machine, generation counter, and append-only ledger this page triages.
- Event-ledger projection model — the architecture the ledger read-path folds under.
- Idempotency and safe retries —
the retry discipline the
Idempotency-Keyheader extends. - Error Codes reference — the
CONFLICTrow and the full retry-safety classification.