Skip to content

Failed payments, retries and refunds

Optional. Worth it when payment can take a while — a reservation stops the holder spending the same voucher somewhere else mid-checkout.

const { token } = await bm.vouchers.reserve({ code });
try {
await charge(customer, voucher.priceUsd);
await bm.vouchers.redeem({ ticket: token, externalRef: order.id });
} finally {
await bm.vouchers.release({ ticket: token, reason: 'payment_failed' });
}

Two things about that finally:

  • release after a successful redeem is not an error. There is nothing left to release, and it answers { released: false }. That is exactly why it is safe there.
  • A reservation you forget about is a voucher your customer cannot use. It lapses on its own after fifteen minutes, but fifteen minutes is a long time at a till.

Send the same externalRef every time. See the five-minute guide — it is the single most common way to get this wrong, and the symptom is a customer losing a second voucher to a network blip.

The customer refunds the order. What happens to the voucher?

It is not un-burned. The chain says this voucher was redeemed against your order on that date, and that remains true — a refund is a later fact, not a correction of an earlier one. Rewriting a sealed ledger because the story continued is exactly the thing our customers pay us not to do.

What you can do instead: ask us to issue the customer a fresh voucher. A reissue is a new row, linked to the old one, and both are visible. It is honest about what happened, where an erased redemption would not be.

statuswhat it means for you
Activeusable
Reservedheld behind a live ticket — yours, if you reserved it
Redeemedalready used. inspect still answers; redeem replays the receipt
Expiredpast validUntil
Voidcancelled by the issuing brand
Distributedthis row was handed on; the holder’s voucher is a different row