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

# Inline SDK

> A drop-in payment popup with one extra superpower — the customer's saved card lives on their phone, reusable across every Duro merchant.

The inline SDK is `DuroCheckout.open()` — a drop-in script that opens a payment sheet over the merchant's own page, in an iframe, so card data never touches the merchant's DOM. Mechanically it's the [hosted checkout](/payments/checkout) flow rendered as a popup. What makes it different is the **identity bar**.

## The popup flow

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant M as Merchant page
    participant Sheet as Duro sheet (iframe)
    participant ID as Identity service
    participant WA as WhatsApp

    M->>Sheet: DuroCheckout.open({ token })
    Sheet->>M: overlay opens (brand-themed)
    Note over Sheet: "Enter your phone to use<br/>your saved Duro payment"
    Sheet->>ID: phone known?
    alt returning customer
        ID->>WA: send OTP
        WA-->>Sheet: 6-digit code (customer reads)
        Sheet->>ID: verify
        ID-->>Sheet: saved cards → one-tap pay
    else new / guest
        Sheet->>Sheet: card / transfer / USSD form
        Note over Sheet: ☑ "save to my phone" → OTP → tokenise + link
    end
    Sheet-->>M: onSuccess / onClose / onError
```

## Integrate in six steps

The sheet renders the same flow as [hosted checkout](/payments/checkout), in a popup over your page. You still create the session **server-side** with your secret key, then hand the browser only the session token — so the amount can never be tampered with client-side.

<Steps>
  <Step title="Add the SDK">
    Drop the script onto the page with your pay button. It exposes `window.DuroCheckout`.

    <CodeGroup>
      ```html Script tag theme={null}
      <script src="https://js.useduro.com/inline.js"></script>
      ```

      ```bash npm theme={null}
      npm install @duro/inline
      ```

      ```ts ESM theme={null}
      import { DuroCheckout } from "@duro/inline";
      ```
    </CodeGroup>
  </Step>

  <Step title="Create a session (server)">
    Identical to hosted checkout — your server calls `POST /v1/checkout/sessions` and returns just the `token` and `id` to the page. The secret key stays on the server.

    ```ts theme={null}
    // POST /api/pay  (your server)
    app.post("/api/pay", async (req, res) => {
      const r = await fetch("https://api.useduro.com/v1/checkout/sessions", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.DURO_SECRET_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          planId: req.body.planId,
          customerEmail: req.user.email,
          metadata: { orderId: req.body.orderId },
        }),
      });
      const { data } = await r.json();
      res.json({ token: data.token, id: data.id });
    });
    ```
  </Step>

  <Step title="Open the sheet (client)">
    Fetch a token from your server, then call `DuroCheckout.open()` with your **publishable** key. Card entry happens inside the iframe — it never touches your DOM.

    ```ts theme={null}
    document.getElementById("pay").addEventListener("click", async () => {
      const { token } = await fetch("/api/pay", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ planId: "plan_01HFZ8Q…", orderId: "ord_1234" }),
      }).then((r) => r.json());

      DuroCheckout.open({
        key: "pk_test_•••",          // publishable key — safe in the browser
        token,                        // the session your server just created
        onSuccess: (reference) => {
          // soft confirmation — verify on your server before granting access
          window.location.href = `/checkout/confirming?ref=${reference}`;
        },
        onClose: () => console.log("sheet dismissed"),
        onError: (err) => console.error(err),
      });
    });
    ```
  </Step>

  <Step title="Verify on success (server)">
    `onSuccess` fires in the browser, so confirm it server-side with the session `id` before acting. Source of truth, every time.

    ```ts theme={null}
    // GET /checkout/confirming → your server
    async function isCompleted(sessionId: string) {
      const res = await fetch(
        `https://api.useduro.com/v1/checkout/sessions/${sessionId}`,
        { headers: { Authorization: `Bearer ${process.env.DURO_SECRET_KEY}` } },
      );
      const { data } = await res.json();
      return data.status === "completed" ? data : null;
    }
    ```
  </Step>

  <Step title="Receive the webhook">
    The popup can be closed before `onSuccess` ever runs — so the **webhook is your reliable trigger**. Subscribe `subscription_payment_success` in **Developers → Webhooks** and verify the signature against the raw body. (Full handler and signature check in [Webhook Events](/api-reference/webhook-events).)

    ```ts theme={null}
    app.post("/webhooks/duro", express.raw({ type: "application/json" }), (req, res) => {
      const raw = req.body.toString("utf8");
      if (!verify(raw, req.header("Duro-Signature"), process.env.DURO_WEBHOOK_SECRET)) {
        return res.sendStatus(400);
      }
      const event = JSON.parse(raw);
      res.sendStatus(200);
      if (event.type === "subscription_payment_success") fulfil(event.data.subscriptionId);
    });
    ```
  </Step>

  <Step title="Give value">
    Both paths — the `onSuccess` callback and the webhook — funnel into one idempotent `fulfil()` that verifies, then provisions exactly once.

    ```ts theme={null}
    async function fulfil(subscriptionId: string) {
      const order = await orders.bySubscription(subscriptionId);
      if (!order || order.fulfilled) return;          // idempotent
      const paid = await isCompleted(order.checkoutId); // verify
      if (!paid) return;
      await entitlements.grant(order.customerId, paid.planId);
      await orders.markFulfilled(order.id);
    }
    ```
  </Step>
</Steps>

<Warning>
  `onSuccess` is a convenience, not a guarantee — wire the **webhook** and the **verify** call too. The browser can vanish mid-payment; the server-side pair is what actually grants access.
</Warning>

## The "save to my phone" moment is the whole strategy

When a guest pays and ticks **save to my phone**, the sheet:

1. Sends a one-time code to their phone over the **WhatsApp Cloud API**.
2. Verifies the code.
3. Creates (or finds) the customer's **universal identity** keyed to that phone.
4. Links the tokenised payment method to the identity — not to the merchant.

The next time that human checks out *anywhere on Duro*, the identity bar recognises the phone and offers their saved card in one tap. That's the network effect: the saved wallet belongs to the person. [See the identity chapter →](/identity/universal-identity)

## Rails, inline

The same rail set as hosted checkout — card, direct debit, one-time bank transfer, USSD, virtual account — and the same instant **rail-switch on failure** ("Card didn't go through — pay with transfer?"). For transfers, the sheet polls for confirmation; the merchant page just waits on the `onSuccess` callback.

## Building blocks the frontend needs

The SDK is an iframe + a thin host script. The pieces:

<CardGroup cols={2}>
  <Card title="The sheet" icon="window-maximize">
    Overlay + responsive sheet (bottom-sheet on mobile, centred card on desktop), themed from the merchant's appearance tokens.
  </Card>

  <Card title="OTP input" icon="shield-halved">
    Six-box code field with paste support, a resend timer, and an "open WhatsApp" deep link.
  </Card>

  <Card title="Saved-methods carousel" icon="wallet">
    The identity's cards/mandates after OTP, with last-4 and brand — never the raw token.
  </Card>

  <Card title="Callback bridge" icon="code">
    `postMessage` to the host page: `onSuccess`, `onClose`, `onError`.
  </Card>
</CardGroup>

Next: the [universal identity](/identity/universal-identity) that powers all of this.
