Reissue a Stripe coupon (replace code or expiry)
Stripe promotion codes are immutable for the code string and the expires_at timestamp after creation. To change either, you deactivate the existing one and create a new one pointing to the same underlying coupon (which is mutable for name and metadata only).
When to use this
- Spam-filter rejection on the existing code (e.g.
AGENCY3FREE→AGENCY3because "FREE" in caps trips SpamAssassin). - Marketing wants to extend or shorten the validity window (
expires_at). - Campaign attribution: a single coupon (the discount rule) backing multiple promotion codes for different channels (LinkedIn vs Slack vs partner site), each with its own
metadata.campaign.
Underlying model
Stripe has two separate concepts:
| Object | Mutable fields | Notes |
|---|---|---|
| Coupon | name, metadata, currency_options | The discount rule (percent_off, duration, applies_to). Created once, reused forever. |
| Promotion code | active, metadata, customer | The customer-facing string + restrictions. Wraps a coupon. |
So you create the coupon once, then create N promotion codes pointing to it.
Steps to swap a code
# 1. Deactivate the existing promotion code
curl -X POST "https://api.stripe.com/v1/promotion_codes/$OLD_PROMO_ID" \
-u "$STRIPE_SECRET_KEY:" \
-d "active=false"
# 2. Create the new promotion code, same coupon, new code/expiry
curl -X POST "https://api.stripe.com/v1/promotion_codes" \
-u "$STRIPE_SECRET_KEY:" \
-d "promotion[type]=coupon" \
-d "promotion[coupon]=$COUPON_ID" \
-d "code=AGENCY3" \
-d "expires_at=1780358400" \
-d "metadata[campaign]=agency_launch_2026_05" \
-d "metadata[replaces]=AGENCY3FREE_spamfilter"
The MCP shortcut for the second call:
mcp__plugin_stripe_stripe__stripe_api_execute({
stripe_api_operation_id: "PostPromotionCodes",
parameters: {
"promotion.type": "coupon",
"promotion.coupon": "<coupon_id>",
code: "AGENCY3",
expires_at: 1780358400,
"metadata.campaign": "agency_launch_2026_05",
},
});
Always-do checks
-
expand[]=applies_towhen reading the coupon — Stripe hides theapplies_to.productsfield by default, so a default GET /coupons/X looks like the coupon discounts everything, when in fact it's correctly scoped. Always passexpand[]=applies_towhen verifying scope:curl "https://api.stripe.com/v1/coupons/$COUPON_ID?expand[]=applies_to" -u "$STRIPE_SECRET_KEY:" -
Check
times_redeemedbefore deactivating the old code. Existing redemptions keep their discount per Stripe's rules even after deactivation, but counting how many is useful for campaign reporting (metadata.campaignlookup later). -
Communicate the rotation to anyone running outreach copy. Old codes silently fail with "Promo code is no longer active" — your sales team should not be quoting
AGENCY3FREEafterAGENCY3is live.
Spam-filter cheat sheet
Avoid in promo codes:
FREEin any case (high SpamAssassin score)WIN,PRIZE,100%OFF- All-caps long strings (use 6-12 chars, mixed letters + numbers)
Generally safe patterns: AGENCY3, EARLYBIRD, LAUNCH99, Q2-PARTNER. Stripe accepts a-z, A-Z, 0-9, dashes; case-insensitive uniqueness is enforced across active codes only.