> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paysight.io/llms.txt
> Use this file to discover all available pages before exploring further.

# External button checkout & upsell

> Host-controlled pay CTAs across checkout and upsell pages

Use `hidePaymentButton: true` with host-rendered buttons for a funnel-style checkout and upsell experience.

<Card title="Live demo" icon="play" href="https://payment.paysight.io/demo?preset=external-button">
  Open the interactive external-button demo in the sandbox playground.
</Card>

## Page 1 — checkout with external button

```html theme={null}
<div id="widget"></div>

<label>
  <input type="checkbox" id="single-purchase" />
  One-time purchase only (no subscription)
</label>

<button id="pay-btn" type="button" disabled>Pay $29.99</button>

<script src="https://payment.paysight.io/widget-sdk.js"></script>
<script>
  const widget = PaySightSDK.createWidget({
    targetId: 'widget',
    config: {
      productId: YOUR_PRODUCT_ID,
      sessionId: 'checkout-' + Date.now(),
      environment: 'sandbox',
      amount: 29.99,
      hidePaymentButton: true,
      savePaymentMethod: true,
      customer: { email: 'customer@example.com', country: 'US' },
    },
    onMessage: (msg) => {
      if (msg.type === 'READY') {
        document.getElementById('pay-btn').disabled = false;
      }
      if (msg.type === 'PAYMENT_SUCCESS') {
        sessionStorage.setItem('paysightSession', msg.payload?.paysightSession ?? '');
        window.location.href = '/upsell';
      }
      if (msg.type === 'ERROR') {
        console.warn(msg.payload.code, msg.payload.message);
      }
    },
  });

  document.getElementById('single-purchase').onchange = (e) => {
    widget.update({ singlePurchase: e.target.checked });
  };

  document.getElementById('pay-btn').onclick = () => widget.submitPayment();
</script>
```

## Page 2 — upsell with external button

```html theme={null}
<div id="upsell-widget"></div>
<button id="upsell-btn" type="button" disabled>Add warranty — $9.99</button>

<script>
  const widget = PaySightSDK.createWidget({
    targetId: 'upsell-widget',
    config: {
      productId: YOUR_PRODUCT_ID,
      sessionId: 'upsell-' + Date.now(),
      environment: 'sandbox',
      amount: 9.99,
      hidePaymentButton: true,
      usePreviousPaymentMethod: true,
      upsell: {
        upsellId: 'warranty',
        initialPaymentSession: sessionStorage.getItem('paysightSession'),
        threeDSRequired: true,
      },
      customer: { email: 'customer@example.com' },
    },
    onMessage: (msg) => {
      if (msg.type === 'READY') {
        document.getElementById('upsell-btn').disabled = false;
      }
      if (
        (msg.type === 'PAYMENT_SUCCESS' && msg.payload?.mode === 'upsell') ||
        msg.type === 'DUPLICATE_TRANSACTION'
      ) {
        window.location.href = '/thank-you';
      }
    },
  });

  document.getElementById('upsell-btn').onclick = () => widget.submitUpsell();
</script>
```

## Error codes to handle

| Code                  | Action                                            |
| --------------------- | ------------------------------------------------- |
| `NOT_READY`           | Keep pay button disabled until `READY`            |
| `VALIDATION_FAILED`   | Show `missingFields` to user                      |
| `INVALID_MODE`        | Use `submitUpsell` on page 2, not `submitPayment` |
| `PAYMENT_IN_PROGRESS` | Disable button during processing                  |

See [Error codes](/widget-sdk/reference/error-codes).

## Related

* [External payment button guide](/widget-sdk/guides/external-payment-button)
* [Saved payment example](/widget-sdk/examples/saved-payment-upsell)
