One-click post-purchase upsells
After the buyer pays, OCart can present an upsell offer that charges the same card with no re-entry. The flow is implemented as an 8-step sequence in OCart_Upsell_Handler::accept().
How the charge works
- Browser POSTs to
/wp-json/ocart/v1/upsells/<offer-id>/acceptwith anX-Idempotency-Keyheader. - Handler validates: offer eligible, cart owns the parent order, parent order paid, signed token valid and within the 30-minute window, single-use
jtinot yet burned. - Resolves the gateway adapter for the parent order via
OCart_Gateway_Registry::for_order(). - Fetches the saved off-session payment method (gateway-side check).
- Initiates an off-session charge with the idempotency key.
- Outcome dispatch:
succeeded,requires_action,declined, orerror. - On success, OCart creates a child order or merges into the parent (configurable strategy).
- Fires the
ocart/upsell/acceptedaction with parent and child order references.
Token security
Upsell tokens are HMAC-signed and short-lived. Default TTL is 30 minutes (upsells.one_click_token_ttl_min in ocart_settings). Each token carries a unique jti that is burned on first use, so replays are blocked at the validation step.
SCA / 3DS handling
If the gateway returns requires_action, the frontend redirects the buyer to the gateway's confirmation step, then resumes the funnel. SCA modal handling is controlled by upsells.sca_modal_enabled (default true).
Card storage
No card data touches your database. Only the gateway-side payment-method ID (for example pm_abc123 from Stripe, or a Paystack authorization code) is stored in WC order meta.
Order strategy
Two options, set with upsells.order_strategy_default:
- Strategy A (default) - child order. New WC order created for the upsell.
- Strategy B - merge into the parent line items, if within
merge_cutoff_min(default 5 minutes). Falls back to Strategy A after that.

