Dynamic coupon offers
Select a Stripe Coupon through a dedicated link, display the offer on the homepage, and apply it automatically when creating Checkout.
Saavo includes link-based dynamic offers. When a user opens /offer/:id, the server remembers the selected offer and the homepage displays its price and promotional text. When the user purchases the specified plan, the system automatically includes the configured Stripe Coupon in Checkout.
Currently, config/products.ts contains no dynamic offers. The entry point exists, but there are no offers by default. To use this feature, create a Coupon in Stripe, then add its Coupon ID and a public offer ID to the relevant plan's couponOffers.
Users do not enter these coupon codes
Dynamic offers are selected through dedicated links. Users do not manually enter a code on the website. The link's id is a public campaign identifier. The value actually sent to Stripe is the server-configured coupon.
How it works
A complete flow is:
The user opens /offer/autumn-20-off
→ The server checks that the offer exists and has not expired
→ Set an HttpOnly cookie valid for 30 minutes
→ Redirect to the homepage in the current language
→ The homepage displays the offer price and copy for the plan
→ The user creates a Checkout Session
→ The system sends the offer's Coupon ID to Stripe
→ Clear the offer cookie after successfully creating the Checkout SessionThe project supports these features by default:
| Feature | Current behavior |
|---|---|
| Offer entry point | GET /offer/:id, with language prefix support |
| Selection record | saavo_coupon_offer cookie, valid for 30 minutes |
| Homepage display | Can override a specified plan's price, labels, selling points, and purchase section text |
| Checkout | Automatically applies the matching offer's Stripe Coupon |
| Expiration | Optional offer deadline through expiresAt |
| Invalid offers | Clears the old selection and redirects to the homepage without an error page |
Responses set Cache-Control: private, no-store to prevent pages with individual offer selections from entering shared caches.
Create a Coupon in Stripe
First, create a Coupon in the Stripe Dashboard and determine its percentage or amount discount, currency, eligible products, duration, and redemption limits. After saving, record the Coupon ID, such as coupon_launch_20_off.
You need a Stripe Coupon ID, not a Promotion Code that users enter. The project only applies existing Coupons to Checkout. Configuration does not automatically create, update, or deactivate Stripe Coupons.
The website's offer deadline and the Stripe Coupon's validity rules are independent. A Coupon may remain valid in Stripe after expiresAt. Conversely, if the Stripe Coupon becomes invalid while the website offer remains active, Checkout creation fails. Configure both separately and keep them consistent.
Configure dynamic offers
Find the participating plan in config/products.ts and add couponOffers:
license: {
type: 'recurring',
interval: 'month',
intervalCount: 1,
priceId: 'price_hobby_monthly_replace_me',
salePrice: '$19',
// Other plan settings omitted
couponOffers: [
{
id: 'autumn-20-off',
coupon: 'coupon_launch_20_off',
expiresAt: Date.parse('2026-10-01T00:00:00Z'),
salePrice: '$15.20',
listPrice: '$19',
discountLabel: { value: '20% off for a limited time' },
badgeLabel: { value: 'Autumn offer' },
valueHint: { value: 'Offer ends September 30, 2026' },
},
],
},couponOffers can contain multiple offers, but each offer can belong to only one plan.
Required settings
| Field | Description |
|---|---|
id | Public offer ID used in /offer/:id links |
coupon | Stripe Coupon ID submitted when creating Checkout |
id must be unique across the entire product catalog, contain at most 64 characters, and use only lowercase letters, digits, and single hyphens, as in autumn-20-off.
Each dynamic offer within a plan must use a different Coupon, and none may use the plan's default coupon. Invalid configurations cause an error during startup or build.
Optional settings
expiresAt is a Unix timestamp in milliseconds representing an exclusive deadline: the offer becomes invalid as soon as the current time reaches it. Without this setting, the offer has no expiration.
Offers can also override these display fields:
title,description,features.salePrice,listPrice,discountLabel,billingLabel.savingsLabel,badgeLabel,valueHint.- Purchase titles, descriptions, fact lists, buttons, specifications, and highlights in
presentation.
These fields change only page presentation. They do not change the Stripe Price, actual discount, or permissions the user ultimately receives. You must calculate and enter display amounts based on the Coupon rules. The project does not automatically calculate salePrice from a discount percentage.
To prevent offer links from changing product structure, dynamic offers cannot override:
priceId, plan type, or billing cycle.- Roles, entitlements, or trial periods.
- Recommended status, default selection, or whether repeat purchases are allowed.
- Plan enabled status or nested
couponOffers.
Create and distribute offer links
After configuring and deploying, you can distribute offer links directly:
https://example.com/offer/autumn-20-offTo specify a language, use its prefix:
https://example.com/zh-Hans/offer/autumn-20-offOn a successful visit, the user is redirected to the homepage in the same language. If the offer ID does not exist, has expired, or is malformed, the system also redirects to the homepage and clears any previous dynamic offer selection from the browser.
Configuration currently supports only an end time, not a start time. If an offer needs to start on a schedule, publish its link or deploy its configuration at the start time. You cannot schedule it in advance with a startsAt field, which is not available.
When an offer selection expires
The saavo_coupon_offer cookie has these attributes:
HttpOnly: Frontend scripts cannot read or modify it.SameSite=Lax: Allows users to arrive through external marketing links.Secure: Sent only over HTTPS in production.Path=/: Both the homepage and Checkout can read it.Max-Age=1800: Retains the selection for 30 minutes.
The server rechecks that the offer exists and has not expired every time it uses the selection. Forging a cookie cannot apply an unconfigured Coupon.
Different cases are handled as follows:
| Case | Discount used | Cookie handling |
|---|---|---|
| Offer matches the current plan | Dynamic offer's coupon | Cleared after successful Checkout creation |
| No offer selected | Plan's default coupon, or none applied automatically if absent | Not written |
| Offer does not exist or has expired | Falls back to the plan defaults | Cleared immediately |
| Offer belongs to another plan | Current plan uses its defaults | Retained for now |
| Stripe rejects the Coupon and Checkout creation fails | No Checkout created | Retained for a retry after correction |
The selection is consumed after successful Checkout Session creation, not after successful payment. If the user reaches Stripe and abandons payment, the website's offer cookie has already been cleared, but the existing Checkout Session retains the Coupon.
How this differs from other discounts
A plan can have a fixed coupon that automatically applies the same Stripe Coupon to every purchase of that plan. A matching dynamic offer overrides this default with its own coupon.
stripe.enablePromoCodes in config/payment.ts controls whether Stripe Checkout allows users to enter a Promotion Code manually. Whenever Checkout already includes a fixed Coupon, whether from the plan default or a dynamic offer, the project disables Promotion Code entry because Stripe does not allow both methods in the same submission.
Choose a discount method using these guidelines:
- Long-term discounts for all buyers: Use a plan-level
coupon. - Special prices distributed through ads, email, or partners: Use
couponOffers. - Codes users enter themselves, distributed publicly or privately: Enable
enablePromoCodesand do not preset a Coupon for that Checkout.
Current limitations
Dynamic offers currently integrate only with homepage product presentation and regular payment Checkout. Upgrade Checkout in the OAuth2 authorization flow still reads the plan's default Coupon and does not use dynamic offers selected through /offer/:id.
Homepage JSON-LD product structured data also continues to use default plan prices, excluding dynamic display prices for individual requests.
The project also has no built-in support for:
- Automatically creating or deactivating Stripe Coupons.
- Offer start times, claim limits, or per-user usage limits.
- An admin dashboard for dynamic offers.
- Tracking visits, Checkout creation, and purchase conversions by offer ID.
Transaction records synchronized through Stripe callbacks retain actual discount amounts, but do not store dynamic offer IDs as separate marketing attribution fields. If you need campaign-level reports, design attribution and analytics separately. Do not rely solely on the saavo_coupon_offer cookie.
This cookie is written when a user deliberately opens an offer link. The current cookie consent component's category switches do not control it. Before launch, update the cookie policy for its actual use and check the explanation in Cookie consent management.
Pre-launch checks
- The Coupon exists in Stripe, and its ID exactly matches
couponOffers[].coupon. - Its discount, currency, eligible products, duration, and limits match the plan rules.
- The offer
idis globally unique and follows the lowercase letter, digit, and hyphen format. -
expiresAtuses milliseconds, and its timezone and cutoff time have been checked. - Page display prices match actual amounts in Stripe Checkout.
- Offer links redirect to the correct language's homepage and display the expected text.
- Purchasing the correct plan automatically applies the offer Coupon, without applying it to other plans.
- Failed Checkout creation retains the offer selection, while successful creation clears it.
- Fixed Coupons and
enablePromoCodesinteract as intended for the product. - The cookie policy describes the purpose and lifetime of
saavo_coupon_offer.
Frequently asked questions
Next steps
- Configure products, Prices, and Stripe: Payments and plans
- Check the offer selection cookie: Cookie consent management
- Track Checkout creation and purchase conversions: Analytics
- Configure test and production credentials: Production configuration and secrets