ServicesPromo Code

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 by per_code and 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 are 409 (or 403 for 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 merchant capability or a platform admin token; redemption is open to any authenticated user. Batches are visible only to their creator (and admins) - anyone else gets a 404, not a 403.

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 10

Creation 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 US

A 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_id and 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 is 409 claim_window_closed; a geo-fenced batch rejects missing or unlisted countries with 403 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 20

Verified: 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 US

Quarantine 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

On this page