# Upselling on the confirmation page

Show product recommendations after the customer has paid and let them add items to the existing order in one tap — enabling it, feeding it recommendations and the two ways to create the order in your own system.

Upsell puts product recommendations on the confirmation page, after the customer has paid.
Because the payment is already authorized, adding an item takes one tap — no payment details,
no second checkout, no second order. Kustom owns the widget, the authorization increase and
the order update. You decide what is recommended and, more consequentially, when your own
system creates the order. That last decision is the real work on this page.

An integration has three parts, and all three are needed before you go live: turn upsell on,
choose where recommendations come from and build your backend to accept an order that can
still change after the customer lands on your confirmation page.

## Before you start

**Upsell must be enabled on your merchant account.** Contact Kustom. Until it is, nothing on
this page has any effect.

You also need to understand one consequence before you write any code: when upsell is
active, **the confirmation push is delayed** until the upsell window closes. Anything your
system does on the push — creating the order, reserving stock, emailing the customer —
happens later than it used to: ten minutes by default, plus a minute's buffer. Plan for that
before you enable it.

The delay applies only when upsell is active for the order **and** the payment method the
customer used allows it — Kustom checks that for you and only loads the widget when it does. If
either is false, the push fires immediately and nothing about your existing
flow changes.

## Step by step

### 1. Turn it on

Per order, which is what you want for a staged rollout or an A/B test:

```json
{
  "options": {
    "confirmation_page_upsell": true
  }
}
```

The flag can still be changed on an update, right up until the order reaches
`checkout_complete`.

Two other things also switch upsell on for an order, and it's worth knowing both before you
wonder why a push arrived late:

- **Setting `merchant_urls.upsell`.** An order carrying an upsell URL has upsell on, with or
  without the option.
- **Account-level enablement.** Kustom can turn upsell on for every eligible order on your
  merchant ID. Once that's set, `confirmation_page_upsell: false` doesn't opt an order out —
  ask Kustom to change the account setting instead.

### 2. Choose where recommendations come from

| | Curation in Kustom Portal | Your own endpoint |
|---|---|---|
| Effort | No development | Build and host an HTTP endpoint |
| How it works | Kustom imports your Google product feed and you pick the products to recommend | Kustom calls your endpoint and you return the products |
| Control | A fixed selection | Per order, dynamic, personalized |
| Best for | Getting started, a small set of evergreen add-ons | An existing recommendation engine or catalog-driven logic |

**Curation in Portal** needs nothing built. Connect your Google product feed under **Upsell**
in [Kustom Portal](https://portal.kustom.co/) and choose the products to recommend. Kustom
still filters the selection for each order: a product is dropped when its currency doesn't
match the order's, when it's out of stock, when it costs more than the authorization headroom
or when the customer already has it in the order. Beyond that the selection is fixed — it
doesn't depend on what else is in the basket or on the chosen shipping option. If you need
that, build an endpoint.

**Your own endpoint.** Set `merchant_urls.upsell` and Kustom calls it before the
confirmation page loads:

```json
{
  "merchant_urls": {
    "upsell": "https://www.example.com/api/upsell"
  }
}
```

**Curated products win.** If your account has Portal-curated products that survive the
filtering above, Kustom shows those and doesn't call your endpoint. Your endpoint is called
only when there is no curated selection for the order. With neither, Kustom asks its own
recommendation service instead.

**Aim to respond in under 3 seconds.** The hard limit is **10 seconds**, the same deadline
every merchant callback gets, and there's no retry, so one slow answer is one lost widget.
But the customer is waiting on a confirmation page while you think, and three seconds is
about as long as that stays comfortable. Treat 3 s as your budget and 10 s as the cliff.

A slow or failed response means no widget — the customer sees an ordinary confirmation page.
The push is still delayed for the full window, though: that delay is decided at completion from
the upsell settings and the payment method, before your endpoint is ever called, and nothing
about the call's outcome shortens it.

### 3. Return recommendations

Kustom `POST`s an `upsell_request` describing the order and the headroom available, and
expects a list of lines back. The [full contract](/checkout/use-cases/upsell-callback-contract/) is a page of its own.
Four fields in the request are what make a recommendation good rather than merely valid:

- **`max_upsell_amount`** — the authorization headroom, in minor units. Filter to what fits. Anything above it will be blocked when the customer taps.
- **`purchase_currency` and `locale`** — price and describe correctly.
- **`billing_address` and `shipping_address`** — apply the right tax rate.
- **`selected_shipping_option`** — drop anything that would break the delivery method already chosen.

A minimal, valid response:

```json
{
  "upsell_lines": [
    {
      "name": "Baseball Cap",
      "reference": "CAP-SAND-001",
      "quantity": 1,
      "unit_price": 40000,
      "tax_rate": 2500,
      "total_amount": 40000,
      "total_tax_amount": 8000,
      "max_allowed_quantity": 3,
      "image_url": "https://www.example.com/images/cap-sand.jpg",
      "product_url": "https://www.example.com/products/cap-sand"
    }
  ],
  "last_upsell_time": "2024-01-01T12:05:00Z"
}
```

**Your endpoint is called more than once.** Kustom calls it before the confirmation page
loads and again after every item the customer adds, so the widget refreshes as the cart grows.
Make the handler idempotent, and exclude what is already in the order from what you
recommend.

Return `upsell_lines: []` to show no widget at all. You'll still be called when
`upsell_possible` is `false`. An empty array is the correct answer then.

### 4. Decide when your system creates the order

This is the decision that shapes your backend. Without upsell, the checkout order is final
when the customer reaches your confirmation page. With upsell, it can still change until the
window closes. There are two defensible ways to handle that.

**Create on push — recommended, and simpler.** Don't create the order until the
confirmation push arrives. It carries the final `order_lines`, upsold items included, so
your system never modifies an order mid-flight.

1. The payment is authorized and Kustom marks the checkout order `checkout_complete`.
2. Kustom redirects the customer to your confirmation page.
3. Your backend reads the checkout order but doesn't create an order yet.
4. You render the Kustom confirmation snippet.
5. Kustom fetches recommendations and shows the widget.
6. The customer adds items. Kustom increases the authorization and updates the checkout order.
7. The window closes and Kustom sends the confirmation push with the final `order_lines`.
8. Your backend creates the order once, from the push.

**Create on redirect.** Create the order in your system as soon as the customer lands on
the confirmation page, on hold, then take updates as items are added. Choose this only when
you genuinely need the order visible immediately — high-value goods where stock must be
reserved the instant payment clears.

1. The payment is authorized and Kustom marks the checkout order `checkout_complete`.
2. Kustom redirects the customer to your confirmation page.
3. Your backend reads the checkout order and creates it in your system, **on hold**.
4. You render the Kustom confirmation snippet.
5. Kustom fetches recommendations and shows the widget.
6. The customer adds an item. Kustom runs your validation callback if you set one, increases
   the authorization and updates the order, then notifies you — at
   `merchant_urls.upsell_notification` if it's set, otherwise at the `notification_uri` your
   upsell response returned. Re-read the order through the Order Management API.
7. The window closes and Kustom sends the confirmation push.
8. Your backend reads the final order and releases the hold for fulfillment.

If you do choose it: **the order must stay updatable for the whole upsell window.** Don't
send it to the warehouse and don't send a confirmation email until the push arrives, or
you'll ship an order that is about to change.

Either way, you can tell whether to expect a delayed push from `options.confirmation_page_upsell`
on the order, or from `upsell_possible` in the upsell request.

## Check that it works

In Playground, with upsell enabled on the account:

1. Create an order with `options.confirmation_page_upsell: true` and complete a purchase.
2. Your `merchant_urls.upsell` endpoint is called before the confirmation page renders, and the request carries `upsell_possible: true` and a non-zero `max_upsell_amount`.
3. The widget renders your recommendations, with images.
4. Adding an item doesn't ask for payment details, the order's `order_lines` grow and your endpoint is called again.
5. The confirmation push arrives **after** the window closes, carrying the final lines.
6. For a purchase where `upsell_possible` is `false`, no widget appears and the push arrives immediately.

To find what was upsold, diff the `order_lines` on the original checkout order against the
`order_lines` in the confirmation push. For a signal per item as it's added, set
`feedback_url` on each upsell line.

## Variations

**Validate each add.** Set `merchant_urls.upsell_validation` and Kustom asks before
processing an addition. The body is the ordinary order model plus `upsell_order_lines`,
the lines being requested. Return `2xx` to allow. Anything else blocks it.

```json
{
  "merchant_urls": {
    "upsell_validation": "https://www.example.com/api/upsell-validation"
  }
}
```

**Do not treat that response as final.** The authorization increase can still fail after
you approve. The confirmation push remains the source of truth.

**Close the widget early.** Return `last_upsell_time` in your upsell response and the widget
stops accepting additions at that moment. It doesn't bring the push forward: the push still
waits for the window configured on your account.

**Re-rating shipping after an add.** If your carrier pricing depends on the cart, either
pre-filter in your upsell endpoint so every candidate keeps the current shipping method
valid — the recommended route — or re-rate afterwards and handle a method change by your
own policy, optionally via the `shipping_option_update` callback.

**A/B testing.** Swap the `merchant_urls.upsell` endpoint whenever you like, or split traffic
with `options.confirmation_page_upsell`. Remember that curated Portal products take
precedence over an endpoint, so clear the curation to test an endpoint against it.

**Minimal confirmation.** Upsell is compatible with the `minimal_confirmation` GUI option
and overrides it automatically while the widget is shown.

## Troubleshooting

**The widget never appears.** Either the order [didn't qualify](/checkout/use-cases/upsell-callback-contract/#when-the-widget-is-shown), for example because of the payment method — check
`upsell_possible` — or there was nothing to show: your endpoint was too slow or returned no
lines, or every curated product was filtered out. All of these fail silently by design.

**Your endpoint is never called.** Your account has Portal-curated products, and they take
precedence. Remove the curation if you want the endpoint to decide.

**Additions are blocked.** They exceed the authorization. Pre-filter on
`max_upsell_amount`. Recommending what will be refused costs conversion.

**`selected_shipping_option` is missing.** It's present whenever the order has a selected
shipping option — set from the order's own `shipping_options` (the preselected one, else the
first) or by the [Shipping Assistant](/checkout/shipping/#shipping-assistant). An order that
carries no shipping options at all has nothing to report. Filter on your own shipping knowledge
instead.

**The push is late.** That's the feature. The window defaults to ten minutes, is set per
merchant account by Kustom and can't be changed through the API. The push follows it by about
a minute. `last_upsell_time` closes the widget sooner but doesn't move the push.

**The push is late and you never enabled upsell.** Check whether the order carries
`merchant_urls.upsell`, or whether Kustom enabled upsell for your whole account. Either one
turns it on without the option.

**You want to show a free-shipping threshold in the widget.** Not supported. Apply
threshold logic on your side when you process the final order.

## Related

- [The upsell callback contract](/checkout/use-cases/upsell-callback-contract/) — every field of the request and the response
- [Confirmation and push](/checkout/integrate/confirmation-and-push/) — the push this page delays
- [Create and update an order](/checkout/integrate/create-order/) — where `options.confirmation_page_upsell` is set
- [Checkout options](/checkout/checkout-options/) — the other options that change what the customer sees
- [Shipping Assistant](/checkout/shipping/#shipping-assistant) — one of the two ways a shipping option gets selected
- [Checkout API reference](/api/checkout/) — the order and callback schemas
