Skip to main content
Validate a discount code or gift-card code before applying it at checkout. Both endpoints share a single request envelope (code + shopId + cartTotalCents + currency) so a single unified redeem field on the storefront can swap the URL path based on which kind the buyer typed. Fail-open posture. Backend faults return HTTP 200 with { valid: false, error: "service_unavailable" } — never a 5xx. The redeem field is payment-adjacent and 5xx-ing it would leave the buyer with a broken checkout. The FE renders an inline error + retry CTA instead of crashing.
These endpoints are read-only. They never bump a discount’s timesUsed counter or flip a gift card’s isRedeemed state. Actual redemption happens atomically with payment authorization at the checkout-intent resolver chokepoint when the FE submits the cart with the validated code field set.

When to use this

  • A storefront or partner checkout renders a unified redeem field that accepts either a discount code or a gift-card code, and dispatches to the matching validator based on which path the buyer picks (or based on inline heuristics in the FE)
  • A custom checkout wants to preview the redeem amount in the cart summary before the buyer hits “Pay” — so the buyer sees the same total at validation, at the cart summary, and at the PSP authorization step
  • The FE needs a stable typed error envelope (rather than HTTP status branching) so it can surface inline messages without try/catching every fetch
Call this once when the buyer enters a code, before enabling the “Apply” CTA. The endpoint never mutates state — calling it repeatedly is safe.

Authentication

None — both endpoints are public. Decorated with @Public() on the controller and mounted under /v2/redeem/*.

Shared request body

Both endpoints accept the same RedeemValidationDto payload.

Shared response envelope

Both endpoints return the same union response shape at HTTP 200 for every business outcome. Success — valid: true:
Error — valid: false:

Error codes

The error enum is stable — new codes will only be added, never renamed. FE clients should default to a generic “This code can’t be applied” string when an unknown reason arrives.

POST /v2/redeem/discount/validate

Validates a merchant-issued coupon code against the cart’s total.

Curl example

Success response

Min-purchase failure response

Invalid-code failure response

POST /v2/redeem/giftcard/validate

Validates a gift-card code against the cart’s total. Same request envelope; kind: "giftcard" on success.

Curl example

Success response

Zero-balance failure response

Expired failure response

Schema-validation errors (400)

The only non-200 status either endpoint emits is 400 for structurally malformed requests (missing required field, negative cartTotalCents, code longer than 64 chars, etc.). A malformed request is a structural FE bug that a retry can’t fix — so the fail-open envelope does not apply.

Architecture notes

Reason-code collapsing (discount internals)

The underlying discount engine surfaces a rich internal reason union — code_not_found / code_disabled / code_expired / code_not_yet_active / code_exhausted / product_not_in_scope. The public /v2/redeem/discount/validate surface deliberately collapses all six into invalid_code. This follows the Apple / Shopify convention of not leaking promotion-engine internals to the buyer:
  • Telling an enumeration attacker which exact code is disabled vs expired widens the leak surface
  • The FE can’t render anything different for “disabled” vs “expired” anyway — the UX outcome is identical
  • The internal reason is preserved in server logs for the merchant dashboard
min_purchase_not_met is the one discount internal reason that does surface (as min_cart_not_met + minCartCents) — because the FE can render a useful “spend $X more to unlock” prompt the buyer can act on.

Gift-card disambiguation

The gift-card repo’s getGiftCardByCode returns null across multiple failure modes (missing / fully-redeemed / expired). To preserve a useful FE surface, the service runs a best-effort secondary lookup on the null path to disambiguate zero_balance vs expired vs invalid_code. The disambiguation walk is wrapped in its own try/catch so transient gift-card-repo faults cannot escape the fail-open envelope — the worst case is the public reason collapses to invalid_code.

Fail-open: backend errors return HTTP 200

Both validators wrap the engine call in try/catch and return { valid: false, error: "service_unavailable" } at HTTP 200 on any backend fault. The redeem field is payment-adjacent — 5xx-ing it would leave buyers with a broken checkout. The FE renders an inline retry CTA and the buyer keeps going. This matches the PSP-stability backbone discipline (the same posture the resolver/gate path uses). The only exception is request-schema validation — a malformed body returns 400, because a structural FE bug is not something a retry can fix.

Redemption happens at the checkout-intent chokepoint

These endpoints are a precondition for redemption, not the write. The actual mutation (decrementing remaining uses on a discount, marking a gift card as isRedeemed, applying the line-item discount to the order) happens server-side at the checkout-intent resolver chokepoint when the FE submits the cart with the validated code field set. That single chokepoint design means:
  • No double-redemption is possible — the redemption write is atomic with payment authorization
  • These validators can be called repeatedly without side effects
  • The FE never has to “release” a code on cart-abandonment — nothing was ever locked

Known v1 limitation: line items not in the unified envelope

The unified RedeemValidationDto does not carry line items. Discounts scoped to specific products (PRODUCT_IDS scope) will therefore validate as invalid_code on this surface — the public collapse layer doesn’t expose product_not_in_scope separately. This matches the v1 shape of the shop-builder unified redeem field, which doesn’t submit line items. If you need product-scoped discount validation today, call the original POST /v2/discounts/validate endpoint — it carries lineItems and returns the granular reason union directly.

Checkout payment-intent resolver

The chokepoint where validated discount + gift-card codes are atomically redeemed against payment authorization. These validators are a precondition — the resolver is the write.

Validate a discount code (granular)

The original discount-only validator. Carries lineItems and returns the full reason union (code_expired / code_not_yet_active / product_not_in_scope / etc.) without the public collapse layer. Use when you need product-scope validation.

Resume an abandoned cart

The storefront-side endpoint customers hit from the recovery email link. Often paired with redeem-field pre-validation when a recovery email carries a bounce-back coupon.

Order lifecycle

Where the resolver’s atomic redemption write fits into the full order timeline (cart → intent → PSP authorization → order created).