Entitlement Locks
A redemption is instant and irreversible. When a code funds a multi-step flow - a trade, a checkout with a payment step, an entitlement transfer - you want to reserve the code first, do the risky work, and only then commit it. That is the lock flow: a two-phase claim with a TTL, so an abandoned flow releases the code automatically.
Locks apply to single-use, non-stored-value codes only; multi-use and
stored-value codes reject lock with 400 invalid_request.
The flow
snug promo-code lock --code SPRING-7K9Q2M4P8X-VIP --ttl-seconds 300{
"lock_id": "plock_mPFnMKnnYHQH",
"commit_token": "ctok_pMtZAdXpAyBS",
"expires_at": "2026-08-28T12:51:04.065857Z",
"status": "intent_locked"
}Hold the commit_token - it is the proof required to finalize, and this
response is the only place it appears. Then either:
snug promo-code commit --lock-id plock_mPFnMKnnYHQH --commit-token ctok_pMtZAdXpAyBS
snug promo-code release --lock-id plock_mPFnMKnnYHQH # on failure/cancelCommit performs the actual redemption: it re-checks batch status, window, and caps, writes the redemption record, and reveals the reward payload. Release clears the reservation without a record, and the code is immediately redeemable again (verified).
Contention and safety
- While a code is locked, both a second
lockand a directredeemfail409 already_locked- the reservation really does exclude everyone, including the holder redeeming outside the lock. - Commit is idempotent: repeating it with the same lock id and token
returns the same
redemption_id. - A wrong commit token fails
400 invalid_request(commit token mismatch); a different user presenting even the correct token fails403 insufficient_permissions- locks are bound to the claimant who created them, and release enforces the same ownership. - A commit can still lose to a cap: if the global or per-user cap was
exhausted between lock and commit, commit fails
409 cap_reached.
TTL and the sweeper
ttl_seconds is capped by PROMO_CODE_MAX_LOCK_TTL_SECONDS (900 by
default; longer requests fail 400 lock_ttl_too_long). Expiry is enforced
inline: the moment a lock's TTL passes, the code redeems and locks normally
again (verified with a 2-second lock), so a crashed flow ties a code up for
at most the TTL. A background sweeper additionally reclaims leftover lock
state every PROMO_CODE_LOCK_SWEEP_INTERVAL_SECONDS (30s default). Size
the TTL to your flow's realistic worst case rather than defaulting to the
maximum.
The exact request and response shapes are in the Promo Code API reference.