# Embeddable checkout widget

The Fynex checkout widget lets you embed the hosted checkout form **directly on your own page** for **test/demo-mode checkout sessions**. Instead of redirecting the customer to `https://pay.fynex.ai/checkout/{session_id}`, you load a small JavaScript SDK that renders the form inside an iframe — on your domain, in your layout.

> [!IMPORTANT]
> Widget iframe embedding is currently enabled only for test/demo-mode payments. Live cardholder checkout pages are protected with same-origin frame headers; use the full-page hosted checkout redirect for live payments.

The widget exposes two modes:

| Mode | Method | Use case |
|------|--------|----------|
| **Embed** | `Fynex.embed()` | Inline form inside a container element on your page. |
| **Popup** | `Fynex.popup()` | Modal overlay on top of your page. |

> [!NOTE]
> For test/demo-mode sessions, the widget is an alternative to the full-page redirect described in [Hosted Checkout](https://api.fynex.ai/payments-api/v2/docs#tag/hosted-checkout). Both modes share the same checkout session lifecycle — you still create the session server-side and verify the outcome via polling or SSE. For live payments, use hosted checkout redirect.

---

## How it works

```
Your backend          Fynex API               Customer browser
────────────          ─────────               ────────────────
POST /checkout  ────► creates session
                      returns sessionId  ◄────
pass sessionId
to frontend

                                         load widget.js
                                         Fynex.embed({ sessionId })
                                              │
                                         iframe → /checkout/{id}?mode=embed
                                              │
                                         customer fills form
                                         (3DS handled inside iframe)
                                              │
                                         postMessage to your page
                                         onSuccess(data) / onFailure(data)

Your backend    ────► poll genericPayment(id) to verify final state
```

---

## Step-by-step

1. **Create a test/demo checkout session from your backend**

   This is identical to the Hosted Checkout flow. `POST /payments-api/v1/checkout` with your Bearer token for a test/demo seller or terminal. See [Hosted Checkout — Step 1](https://api.fynex.ai/payments-api/v2/docs#tag/hosted-checkout) for the full request reference.

   ```bash
   curl -sS -X POST https://api.fynex.ai/payments-api/v1/checkout \
     -H "Authorization: Bearer $FYNEX_TOKEN" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: $(uuidgen)" \
     -d '{
       "externalOrderRef": "ORDER-2099",
       "amount": 79.00,
       "currencyCode": "GBP",
       "countryCode": "GB",
       "returnUrls": {
         "success": "https://example.com/orders/2099/success",
         "failure": "https://example.com/orders/2099/failure"
       }
     }'
   # Response: { "sessionId": "6f9b84e1-...", "checkoutUrl": "...", "expiresAt": "..." }
   ```

   Pass the `sessionId` to your frontend — do **not** expose your Bearer token to the browser.

2. **Include the widget script**

   Add the script tag to your page. It must load before you call `Fynex.embed()` or `Fynex.popup()`. The script is served with `Cache-Control: public, max-age=3600` and `Access-Control-Allow-Origin: *`, so it can be loaded from any domain.

   ```html
   <script src="https://api.fynex.ai/widget/checkout.js"></script>
   ```

   No API key or auth header is needed for the script itself — it is a public static asset.

3. **Launch the widget**

   Choose **embed** (inline) or **popup** (modal) depending on your UX preference.

   **Embed mode** — renders the checkout form inside a container element you control:

   ```html
   <div id="payment-form"></div>

   <script>
     var checkout = Fynex.embed({
       sessionId: SESSION_ID,      // string — from your backend
       container: '#payment-form', // CSS selector or DOM element
       onSuccess: function(data) {
         console.log('Payment succeeded', data);
         // Do NOT fulfil the order here — verify server-side first
       },
       onFailure: function(data) {
         console.log('Payment failed', data);
       }
     });

     // To remove the widget later (e.g. after navigating away):
     // checkout.destroy();
   </script>
   ```

   **Popup mode** — opens a modal overlay over your page:

   ```html
   <button id="pay-btn">Pay now</button>

   <script>
     document.getElementById('pay-btn').addEventListener('click', function() {
       var popup = Fynex.popup({
         sessionId: SESSION_ID,
         onSuccess: function(data) {
           console.log('Payment succeeded', data);
         },
         onFailure: function(data) {
           console.log('Payment failed', data);
         },
         onClose: function() {
           console.log('Customer closed the popup');
         }
       });

       // To close programmatically:
       // popup.close();
     });
   </script>
   ```

4. **Verify the payment server-side**

   The `onSuccess` callback is a client-side signal only — it fires when the iframe posts a `success` message to your page. A determined user could fire a `postMessage` manually. Always verify the final payment state on your backend before fulfilling the order.

   ```js
   // On your server, after onSuccess fires and the browser POSTs your endpoint:
   const { genericPayment } = await gql(
     `query ($id: Int!) { genericPayment(id: $id) { id status } }`,
     { id: paymentId }
   );
   if (genericPayment.status === 'settled' || genericPayment.status === 'provider_completed') {
     fulfillOrder();
   }
   ```

   See [Polling & SSE](https://api.fynex.ai/payments-api/v2/docs#tag/polling-sse) for a complete server-side verification reference.

---

## API reference

### `Fynex.embed(opts)` → `{ destroy() }`

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `sessionId` | string | Yes | The checkout session ID returned by `POST /payments-api/v1/checkout`. |
| `container` | string \| Element | Yes | CSS selector (e.g. `'#payment-form'`) or a DOM element reference. |
| `onSuccess` | function(data) | No | Called when the payment completes successfully. |
| `onFailure` | function(data) | No | Called when the payment fails. |

Returns an object with a `destroy()` method that removes the iframe and cleans up event listeners.

### `Fynex.popup(opts)` → `{ close() }`

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `sessionId` | string | Yes | The checkout session ID. |
| `onSuccess` | function(data) | No | Called when the payment completes successfully. |
| `onFailure` | function(data) | No | Called when the payment fails. |
| `onClose` | function() | No | Called when the overlay is dismissed (close button or click-outside). |

Returns an object with a `close()` method that dismisses the overlay programmatically.

### Events (postMessage)

The widget communicates via `window.postMessage`. The widget SDK handles these internally; you interact through the callbacks above. Messages carry `{ type: 'fynex-checkout', event: 'success' | 'failure' | 'close' | 'resize', data: {} }`.

---

## Common pitfalls

> [!WARNING]
> **Do not fulfil orders from `onSuccess` alone.** The callback fires on a client-side `postMessage`. Always verify the payment state server-side via the GraphQL `genericPayment` query before dispatching goods or services.

> [!CAUTION]
> **Session expiry.** Checkout sessions have a server-side TTL (returned as `expiresAt`). If the customer takes too long, the iframe will show an expiry error. Create a fresh session and re-initialise the widget.

- **`container` not found.** `Fynex.embed` throws `"Fynex.embed: container not found"` if the selector matches no element. Ensure the DOM node exists before calling `embed()`.
- **`sessionId` is required.** Both `embed` and `popup` throw immediately if `sessionId` is missing or falsy.
- **Widget behind a strict CSP.** The iframe loads `https://api.fynex.ai`. Ensure your Content-Security-Policy `frame-src` directive allows `https://api.fynex.ai`.
- **Live checkout sessions.** Live payment pages are not iframe-embeddable by default. If you pass a live `sessionId` to the widget, browser frame protections can block the iframe. Use the hosted checkout redirect for live payments.
- **3DS inside the iframe.** For test/demo widget sessions, 3DS challenges are handled entirely within the embedded iframe — your page does not need to manage any redirect. The `onSuccess` / `onFailure` callback fires once the full payment flow (including any 3DS step) is complete.

## See also

- **[Hosted Checkout](https://api.fynex.ai/payments-api/v2/docs#tag/hosted-checkout)** — Full-page redirect alternative — lowest integration effort.
- **[Polling & SSE](https://api.fynex.ai/payments-api/v2/docs#tag/polling-sse)** — Server-side payment verification after onSuccess fires.
- **[Server-to-server payments](https://api.fynex.ai/payments-api/v2/docs#tag/server-to-server)** — Full control over the payment flow without any hosted page.
