Promo Code
Campaign batches of unique redemption codes with atomic, exactly-once redemption: single-use, multi-use, and stored-value claim types, redemption caps (global, per-user, per-referrer, per-code), claim windows, geo fences, idempotency keys, two-phase entitlement locks, and a quarantine kill switch for leaked batches. Every example on this page was executed against a live server.
When to reach for it: launch giveaways, partner- or creator-distributed reward codes with referral attribution, gift-card-style stored-value codes, region-limited campaigns, and trade flows that must reserve a code before committing it.
When not to: earning and spending loyalty points belongs to Loyalty; recurring entitlement plans belong to Subscription; weighted random prize draws belong to Lottery.
Concepts
- A batch is a campaign - one create call generates up to 100,000
unique codes (unambiguous alphabet, no
0/O/1/I) with an optional prefix/suffix policy, plus the reward payload, caps, window, and fences they all share. - Raw codes are never stored or returned - the server keeps only salted SHA-256 hashes. Redemption hashes the submitted code (case- and whitespace-insensitive) and looks the hash up; export returns hashes, not codes.
- Claim types:
single_use(one redemption, ever),multi_use(shared code, bounded byper_codeand the other caps),stored_value(each code carries a balance debited across redemptions). - Eligibility gates run in order on every redemption: batch status must
be
active, the claim window must be open, the geo fence must pass, and no cap may be exhausted. Failures are409(or403for quarantine and geo) with a machine-readable code. - Reward payloads are hidden until redemption - the payload set at creation is revealed in successful redemption responses; batch reads reveal it only to merchant/admin callers.
- Ownership and roles: creating and operating batches requires a
merchantcapability or a platform admin token; redemption is open to any authenticated user. Batches are visible only to their creator (and admins) - anyone else gets a404, not a403.
Create a batch
Batch management calls below use a merchant or platform admin token.
snug promo-code generate --count 1000 --name spring_promo \
--prefix SPRING --suffix VIP --length 10 \
--reward '{"type":"account_credit","amount":25,"currency":"USD"}'
snug promo-code batch get --batch-id <batch_id>
snug promo-code batch list --page-size 10Creation responds 202 Accepted with a progress_url, but generation is
synchronous - the batch is already active with all codes generated when
the response returns. Caps, claim windows, geo fences, and stored-value
policies are additional JSON fields on POST /api/v1/promo-codes/batches
(see the API reference); over HTTP a capped, windowed batch looks like:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "docs4-spring-promo", "count": 1000,
"claim_type": "multi_use",
"caps": {"per_user": 1, "per_code": 5},
"claim_window": {"ends_at": "2026-09-30T00:00:00Z"},
"reward_payload": {"type": "item_grant", "item": "spring-hat"}}' \
http://localhost:4000/api/v1/promo-codes/batches{
"status": 202,
"msg": "Accepted",
"data": {
"batch_id": "WfWXswnsjtCFhZEGcbdp",
"status": "active",
"requested_count": 1000,
"progress_url": "/api/v1/promo-codes/batches/WfWXswnsjtCFhZEGcbdp"
}
}batch list accepts the shared search grammar
(q over name/description, filter=claim_type:eq:stored_value, sort_by)
over HTTP; non-admin callers only ever see their own batches.
Redeem
snug promo-code redeem --code SPRING-7K9Q2M4P8X-VIP
snug promo-code redeem --code SPRING-7K9Q2M4P8X-VIP --idempotency-key checkout:ord_123
snug promo-code redeem --code SPRING-7K9Q2M4P8X-VIP --referrer-id partner_7 --country USA successful redemption returns committed and reveals the reward payload.
Verified behaviors:
- Redeeming a spent single-use code fails
409 already_redeemed. - Replaying with the same idempotency key returns the original
redemption_idand payload instead of failing - always send one from checkout flows. - An unknown code is a deliberately generic
409 ineligible- the API does not confirm whether a guessed code exists. - A capped redemption (per-user, per-referrer, per-code, or global) fails
409 cap_reached; outside the claim window it is409 claim_window_closed; a geo-fenced batch rejects missing or unlisted countries with403 geo_blocked(pass--country/geo).
Caps can be tightened or raised later without touching committed
redemptions - snug promo-code caps update --batch-id <id> --per-user 2
(PATCH .../caps).
Stored value
A stored-value batch gives every code a balance that is debited across redemptions until exhausted:
snug promo-code generate --count 100 --name gift_cards \
--reward '{"type":"gift_card"}' \
--stored-value-initial 50 --partial-redemption
snug promo-code stored-value redeem --code GIFT-XXXX --amount 20Verified: a 20 debit from a 50 balance returns partially_redeemed with
remaining_value: 30; the debit that reaches zero returns exhausted; an
over-balance debit fails 409 insufficient_stored_value. Stored-value
codes must go through the stored-value endpoint - the plain redeem endpoint
rejects them with 400 invalid_request.
Entitlement locks
For trades and multi-step flows, a single-use code can be reserved with a TTL lock and committed or released later - see Entitlement locks for the full two-phase protocol.
Operations
snug promo-code quarantine --batch-id <id> --reason suspected_leak
snug promo-code resume --batch-id <id> --note reviewed
snug promo-code archive --batch-id <id>
snug promo-code batch export --batch-id <id>
snug promo-code redemptions --batch-id <id> --status committed --country USQuarantine freezes redemption immediately (403 quarantined, verified) and
records an audit event; resume reopens the batch. archive is terminal -
an archived batch cannot be resumed (400) and its codes are permanently
409 ineligible. export returns each code's salted hash and current
status for audit and reconciliation - never raw codes. The redemptions list
filters by status, user, referrer, and country.
Limits and configuration
Up to 100,000 codes per batch, 64 KB reward payloads, 14-character default
code cores, 24-hour idempotency keys, and the lock TTL cap and sweeper
cadence are all tunable via the PROMO_CODE_* variables in the
Promo Code CONFIG reference.
Reference
- Promo Code API - every endpoint, callable
- Related: Loyalty for points programs, Subscription for entitlement plans, Lottery for weighted draws