---
title: Stacked Offers
description: Put two offers in one offer slot and let customers accept both with one click, such as a plan change with a discount or a pause followed by a discount.
---

A stacked offer is two offers in one offer slot. The customer sees both on one card and accepts them with a single **Accept Both Offers** button: for example, a cheaper plan plus a discount on it, or a pause followed by a discount once the subscription resumes.

## Available pairs

The first offer decides which offers can follow it and when the second one starts.

| First offer | Second offer | When the second offer applies |
| --- | --- | --- |
| Switch subscription plan | Discount | Together with the plan change |
| Rebate | Discount, pause, or plan change | Together with the rebate |
| Pause subscription | Discount or plan change | When the pause ends |
| Trial extension | Discount | One day before the extended trial ends |

A discount can't come first, and custom offers, contact and redirect offers can't be part of a pair.

### Billing provider support

| **Pair**                                    | **Stripe** | **Chargebee** | **Paddle Billing** | **Paddle Classic** | **Braintree** | **Maxio** |
| ------------------------------------------- | ---------- | ------------- | ------------------ | ------------------ | ------------- | --------- |
| **Plan change + discount**                  | ✅          | ✅             | ✅                  | ✅                  | ✅             | ✅         |
| **Rebate + discount, pause or plan change** | ✅          | -             | -                  | -                  | -             | -         |
| **Pause + discount**                        | ✅          | ✅             | -                  | -                  | ✅             | -         |
| **Pause + plan change**                     | ✅          | -             | -                  | -                  | ✅             | -         |
| **Trial extension + discount**              | ✅          | ✅             | ✅                  | -                  | -             | -         |

Each offer in a pair needs the same billing provider support it needs on its own, so rebate pairs are Stripe only, and Paddle Classic has no pause pairs. A pair that waits for a pause also needs the second offer to be on the subscription before the first invoice after the pause. Chargebee doesn't allow changes to a paused subscription, so a pause can't be followed by a plan change there. Paddle Billing bills a new period as soon as the subscription resumes, and on Maxio the second offer would go on only after the resume, so neither offers a second offer after a pause.

## Setting up a pair

1. Open an offer in the cancel flow builder: an initial or final offer step, a survey choice's offer, or a follow-up option's offer.
2. Set up the first offer as usual, then click **+ Add a second offer**.
3. Under **Second Offer**, pick the offer type. The list holds only the types that can follow the first offer.
4. Configure the second offer the same way as on its own. A discount in a pair needs a coupon, and a plan change needs at least one plan.

The card's header and description cover both offers. To go back to a single offer, click **Remove second offer**. If you change the first offer's type, the second offer stays when it can still follow the new type and is removed otherwise. AI flow generation and rework don't create pairs, and they keep the pairs you set up where those still fit.

## What the customer sees

A pair shows as one card with both offers. Where the pair includes a pause or a plan change, the customer picks the pause length or the new plan inside the card. A line under each offer says when it starts, such as "Starts when your subscription resumes" under an offer that follows a pause. After accepting, the customer sees one confirmation for both offers, which says so when the second offer waits for a pause or trial to end.

A pair is shown only when the customer qualifies for both offers. If either offer would be skipped on its own (for example, a pause for an annual subscription, or a discount during the discount cooldown), the whole pair is skipped and the flow moves on.

Pairs show in the embed, hosted cancel flows and the React SDK (`@churnkey/react`). Hosted cancel flows don't show rebates, so they skip rebate pairs. The React SDK shows pairs only in a UI that shows both offers: a flow with your own `Offer` component, or a custom component for a built-in offer type, skips pairs unless it also passes a `StackedOffer` component, and the headless `useCancelFlow` hook skips them unless you pass `stackedOffers: true`. Versions of `@churnkey/react` released before stacked offers skip any offer slot that holds a pair. The SDK side is described in its [README](https://github.com/churnkey/sdk/tree/main/packages/react#readme).

## When the second offer applies

**Plan change or rebate first.** Churnkey applies both offers when the customer accepts. A rebate with a move to a cheaper plan on the same billing term isn't prorated, whatever your proration setting: the rebate already compensates the customer.

**Pause first.** A discount applied during a pause would be spent on invoices the customer doesn't pay, so the second offer waits until the subscription resumes:

- When the customer resumes early through Churnkey, for example from the [pause wall](/cancel-flows/pause-wall), the second offer applies right away. On a Stripe subscription in `paused` status, the discount goes on before the subscription resumes, so the first invoice after the pause carries it. While the second offer waits, the pause wall says what reactivating brings: the new plan and its price, or that the discount starts.
- Otherwise Churnkey checks the subscription once a day while the pause lasts. The second offer applies within a day of the pause ending, including a pause you end early or extend outside Churnkey. With Stripe's payment collection pause, a discount goes on during the last paused billing period instead, so the first invoice the customer pays carries it.

On Chargebee, a discount after a pause goes on when the customer accepts: Chargebee doesn't allow changes to a paused subscription, and the coupon carries through the pause to the first invoice after it. The pair is `APPLIED` right away, canceling during the pause drops nothing, and the discount cooldown counts from the pause end.

**Trial extension first.** The discount applies one day before the extended trial ends, so the first paid invoice carries it. If the trial is extended again outside a pair, the discount waits for the new end.

A second offer that waits is dropped when:

- the subscription is canceled before the pause or trial ends. While the subscription is set to cancel, the second offer waits, and it applies only if the cancellation is taken back;
- the customer accepts another pair of the same kind on the same subscription: a pause pair replaces a waiting pause pair, and a trial extension pair replaces a waiting trial discount;
- the pause or trial still hasn't ended 400 days after its planned end. This one counts as a failure and is reported as described below.

A discount in a pair counts toward the [discount cooldown](/cancel-flows/offers/discounts) like any other discount. For a discount that waits for a pause or trial, the cooldown counts from when the discount applies. A pause in a pair counts toward the [pause cooldown](/cancel-flows/offers/pause-subscription#configuration) (**Pause Redemption Cooldown** under **Cancel Flows → Settings → Offer Redemption Settings**) like any other pause.

## When only one offer applies

Before applying either offer, Churnkey checks that your billing system still has the coupon and the price and accepts them: a deleted or archived coupon, or an archived price, applies nothing. On Stripe, Chargebee and Paddle Billing, the check also catches an expired or used-up coupon. If neither offer could be applied, the customer sees an error, as with a single offer. If one offer applied and the other failed, the flow still completes: the customer sees what applied, which offer couldn't be, and that you'll handle it, for example "Plan changed. We couldn't apply your discount. We'll handle it and let you know." Churnkey doesn't retry the offer that failed, so apply it in your billing system or let the customer know.

Churnkey emails the owners and admins of your account when part of a live pair fails while Churnkey applies it, either when the customer accepts or later, when a second offer waited for a pause or trial. The email names the customer, what applied, what didn't and why, and links to the customer and to every such session. You get at most 20 of these emails a day; turn them off under **Settings → Integrations → Email Reports**.

To see every pair that didn't fully apply, filter the Activity Stream by **Stacked Offers → Not all applied**.

The session carries the failure too: the [session webhook](/data-integrations/webhooks), the Slack message and the Data API have it in `acceptedOffer.stackedOffer` (see below). A second offer that fails later, after a pause or trial, is also reported by a separate [`stacked_offer_failed` webhook event](/data-integrations/webhooks#stacked-offer-failed) and a [Slack](/data-integrations/slack) message, because its session was sent long before.

## Custom handler callbacks

If you set [handler callbacks](/cancel-flows/further-configuration#handler-callbacks) such as `handleDiscount` or `handlePlanChange`:

- **Pairs that apply together** (plan change or rebate first) are shown when you set handlers for both offers' types or for neither. A pair with a handler for only one of them is skipped, because Churnkey applies an offer of a pair only together with the other one. With both handlers, each offer goes through its own. The first offer runs first, then the second, except that a rebate always runs last, so one accept can call two of your handlers. A handler that redirects or closes the page after the first one stops the second. If only the one that runs second rejects, the flow completes with the note above and the session records the failure.
- **Pairs with a pause or trial extension first** are skipped when you set a handler for either offer's type, because the wait happens on Churnkey's side. This includes a pause and discount on Chargebee, where the discount goes on at accept.
- Listener callbacks such as `onDiscount` fire once for each offer applied at accept. A second offer that applies later, after a pause or trial, fires none.

In Direct mode, where every offer needs its handler, only pairs that apply together are shown, and only when both of their handlers are set.

## Session data

The session stores the first offer in `acceptedOffer` as usual, and the second offer in `acceptedOffer.stackedOffer` with the same fields plus these:

| Field | Meaning |
| --- | --- |
| `stackStatus` | `APPLIED` when both offers applied, `SCHEDULED` when the second waits for a pause or trial to end, `FAILED` when one of them didn't apply, `CANCELED` when the waiting second offer was dropped. |
| `stackFailureReason` | With `FAILED` or `CANCELED`: the offer that didn't apply and why, for example `DISCOUNT: No such coupon` or `DISCOUNT: canceled`. |
| `acceptanceId` | Churnkey's id of the accepted pair. |
| `customActionHandler` | The handler callback that applied the second offer, when one did. |

When a second offer that waits applies, the session's `stackStatus` changes from `SCHEDULED` to `APPLIED`. When it can't be applied, the status changes to `FAILED` with the reason. When it's dropped because the subscription ended or a later pair replaced it, the status changes to `CANCELED`: there's nothing to apply by hand, so these sessions stay out of the **Not all applied** filter. The dashboard, the CSV export and the Data API show the updated status; the session webhook isn't sent again. `presentedOffers` entries carry the second offer in `stackedOffer` too.

The sessions CSV export adds the columns `stackedOfferType`, `stackedOfferStatus`, `stackedOfferFailureReason`, `stackedPauseDuration`, `stackedCouponId`, `stackedCouponType`, `stackedCouponAmount`, `stackedCouponDuration` and `stackedNewPlanId` at the end.

The session result and cancel flow analytics count a pair under its first offer: a pause followed by a discount counts as a pause. Boosted revenue and coupon filters don't include the second offer yet.

## Testing

Use **Preview** in the builder to check the card. In **Test Mode**, accept a pair as a test customer and check both changes in your billing provider. For a pair that waits for a pause, resume the test subscription and check that the second offer applies within a day.

## Limitations

- Two offers per slot at most.
- The lines Churnkey adds to a pair (the button, the timing lines and the confirmation) come in English, German, French, Spanish, Chinese, Arabic, Portuguese, Russian, Japanese, Turkish, Vietnamese, Korean, Italian, Polish, Romanian, Greek, Dutch and Czech in the embed and hosted cancel flows; flows in other languages show them in English. The React SDK has English defaults, which you replace through its messages like its other texts.
- Analytics count a pair under its first offer.
- Hosted cancel flows skip rebate pairs.
- The discount in a pair uses a coupon you pick. It can't be an [Adaptive Offer](/cancel-flows/adaptive-offers): adaptive discounts learn from customers who see a discount on its own, and a discount that comes with a pause or a plan change isn't comparable.
