Upsell API
Post-purchase upsell flow management with targeting rules
Upsell API
The Upsell API enables post-purchase 1-click upsells. After a successful checkout, create an upsell session with targeted offers that customers can accept without re-entering payment details.
Flow Overview
1. Customer completes checkout
│
v
2. Create upsell session with offers
│
v
3. Customer sees first offer
│
┌───┴───┐
│ │
Accept Decline
│ │
v v
4. Charge Next offer
saved (or exit)
payment
│
v
5. Repeat until no more offersCreate Upsell Session
Create an upsell session with targeted offers.
POST /api/upsell/create-session
Content-Type: application/jsonRequest Body
interface CreateSessionRequest {
orderId: string; // New order ID for upsells
originalOrderId: string; // Original checkout order ID
customerEmail: string;
customerId?: string;
paymentMethodToken?: string; // Token from checkout completion
provider: PaymentProviderType;
currency?: string; // Default: "USD"
offers: UpsellOffer[];
metadata?: Record<string, string>;
// For targeting rules
cartTotal?: number; // Original cart total in cents
purchasedProductIds?: string[];
// Theming
themeId?: string; // Theme to apply to upsell pages
}
interface UpsellOffer {
id: string;
name: string;
description?: string;
price: number; // In cents
originalPrice?: number; // For showing discount
image?: string;
showIf?: TargetingRules;
}
interface TargetingRules {
minCartValue?: number; // Minimum cart value in cents
productIds?: string[]; // Show only if purchased these
excludeProductIds?: string[]; // Hide if purchased these
}Example Request
{
"orderId": "upsell_order_123",
"originalOrderId": "order_abc123",
"customerEmail": "customer@example.com",
"paymentMethodToken": "pm_1ABC|cus_XYZ",
"provider": "stripe",
"currency": "USD",
"offers": [
{
"id": "offer_1",
"name": "Premium Upgrade",
"description": "Upgrade to premium for 50% off",
"price": 4999,
"originalPrice": 9999,
"image": "https://example.com/upgrade.jpg",
"showIf": {
"minCartValue": 2500
}
},
{
"id": "offer_2",
"name": "Extended Warranty",
"description": "2-year extended warranty",
"price": 1999,
"showIf": {
"productIds": ["prod_123"]
}
}
],
"cartTotal": 9999,
"purchasedProductIds": ["prod_123"],
"themeId": "theme_xyz123"
}Success Response
{
"success": true,
"sessionId": "sess_abc123",
"upsellUrl": "/upsell/sess_abc123",
"expiresAt": 1704110400000,
"totalOffers": 2
}No Eligible Offers Response
When targeting rules filter out all offers:
{
"success": true,
"skipUpsell": true,
"message": "No eligible upsell offers for this customer"
}Accept Upsell Offer
Accept an upsell offer and charge the saved payment method.
POST /api/upsell/accept
Content-Type: application/jsonRequest Body
{
"sessionId": "sess_abc123",
"offerId": "offer_1"
}Response
{
"success": true,
"transactionId": "pi_upsell_123",
"offerAccepted": "offer_1",
"amountCharged": 4999,
"hasMoreOffers": true,
"totalUpsellAmount": 4999
}Decline Upsell Offer
Decline the current offer and move to the next one.
POST /api/upsell/decline
Content-Type: application/jsonRequest Body
{
"sessionId": "sess_abc123",
"offerId": "offer_1"
}Response
{
"success": true,
"hasMoreOffers": true,
"nextOfferId": "offer_2"
}Get Session Status
Retrieve current upsell session status.
GET /api/upsell/session/:idResponse
{
"success": true,
"session": {
"id": "sess_abc123",
"status": "active",
"currentStep": 1,
"totalOffers": 2,
"acceptedOffers": ["offer_1"],
"declinedOffers": [],
"totalUpsellAmount": 4999,
"expiresAt": 1704110400000
}
}Session Statuses
| Status | Description |
|---|---|
active | Session is active, offers available |
completed | All offers shown, session complete |
expired | Session expired (30 minute limit) |
External Action Links
For headless or external upsell flows (e.g., triggering an upsell accept from a blog post or custom landing page), use Action Links. These links handle the payment logic securely and redirect the user to the next step.
Format:
https://[YOUR_DOMAIN]/upsell/action/[ACTION]/[SESSION_ID]Supported Actions
| Action | URL Pattern | Description |
|---|---|---|
| Accept | /upsell/action/accept/{sessionId} | Accepts the current offer, processes payment, and redirects to next step. |
| Decline | /upsell/action/decline/{sessionId} | Declines the current offer and redirects to next step or downsell. |
Redirect Behavior
- Success: If there is a "Next Node" in your Flow, the user is redirected there.
- If the next node has an
externalPageUrlproperty, the user is sent to that URL +?sessionId=....
- If the next node has an
- Completion: If the flow is finished, the user is redirected to the
/successpage (or your configuredsuccessUrl).
Example Integration
<!-- On your external landing page -->
<a href="https://checkout.mysite.com/upsell/action/accept/sess_abc123" class="btn btn-primary">
Yes! Add to Order ($19.99)
</a>Targeting Rules
Offers can include targeting rules to show/hide based on the original purchase:
Minimum Cart Value
Show offer only if the original cart total meets a minimum:
{
"showIf": {
"minCartValue": 5000
}
}Product-Based Targeting
Show offer only if customer purchased specific products:
{
"showIf": {
"productIds": ["prod_123", "prod_456"]
}
}Exclusion Rules
Hide offer if customer purchased certain products (e.g., don't offer warranty on a product they already bought warranty for):
{
"showIf": {
"excludeProductIds": ["warranty_123"]
}
}Combined Rules
Rules can be combined. All conditions must be met:
{
"showIf": {
"minCartValue": 2500,
"productIds": ["prod_electronics"],
"excludeProductIds": ["bundle_complete"]
}
}Payment Method Tokens
The paymentMethodToken enables 1-click charging without re-entering payment details.
Token Formats by Provider
| Provider | Format | Notes |
|---|---|---|
| Stripe | pm_xxx|cus_xxx | PaymentMethod ID and Customer ID |
| NMI | vault_xxx|txn_xxx | Customer vault and original transaction |
| Sticky.io | sticky_orderId | Original Sticky order for add-to-order |
| Konnektive | orderId | Original Konnektive order |
Retrieving Token
The token is returned from /api/checkout/complete:
{
"success": true,
"orderId": "order_123",
"paymentMethodToken": "pm_1ABC|cus_XYZ"
}Or retrieve it later:
POST /api/checkout/get-payment-token
Content-Type: application/json
{
"provider": "stripe",
"transactionId": "pi_123"
}Best Practices
Offer Design
- Limit offers: 2-3 offers maximum per session
- Show value: Display original price and discount percentage
- Clear descriptions: Explain what the customer is getting
- Relevant images: Product images increase conversion
Session Management
- Handle expiration: Sessions expire after 30 minutes
- Track analytics: Log acceptance/decline rates
- Test targeting: Verify targeting rules before production
Error Handling
Common error scenarios:
// Session expired
{
"error": "Session expired",
"status": 400
}
// Invalid offer ID
{
"error": "Offer not found in session",
"status": 404
}
// Payment failed
{
"error": "Payment failed: Card declined",
"status": 402
}Code Example
Complete upsell flow implementation:
// After successful checkout
const checkoutResult = await fetch('/api/checkout/complete', {
method: 'POST',
body: JSON.stringify({
orderId: 'order_123',
transactionId: 'pi_abc',
provider: 'stripe',
// ... other fields
}),
});
const { paymentMethodToken, orderId } = await checkoutResult.json();
// Create upsell session
const sessionResult = await fetch('/api/upsell/create-session', {
method: 'POST',
body: JSON.stringify({
orderId: `upsell_${orderId}`,
originalOrderId: orderId,
customerEmail: 'customer@example.com',
paymentMethodToken,
provider: 'stripe',
offers: [
{
id: 'premium_upgrade',
name: 'Premium Upgrade',
price: 4999,
originalPrice: 9999,
},
],
cartTotal: 9999,
purchasedProductIds: ['prod_123'],
}),
});
const session = await sessionResult.json();
if (session.skipUpsell) {
// Redirect to success page
window.location.href = '/success';
} else {
// Redirect to upsell page
window.location.href = session.upsellUrl;
}