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

# Widget

> Embed the hosted payment page, and optionally lend it the wallet your user already connected.

A session comes back with **two** URLs, and they are not interchangeable:

| Field      | Route                | Use it to                                                                                                    |
| ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------ |
| `url`      | `/pay/:id`           | **Redirect** the payer. Deliberately NOT framable — it answers `X-Frame-Options: DENY` as anti-clickjacking. |
| `embedUrl` | `/embed?paymentId=…` | **Embed** in an iframe. Served with no framing headers.                                                      |

```typescript theme={"system"}
redirect(session.url!);                       // redirect
<iframe src={session.embedUrl!} />            // embed
```

<Warning>
  Framing `url` produces a blank box and one console line
  (`frame-ancestors 'none'`), with no request reaching us and nothing in your
  logs. If an embed is blank, check which of the two you used first.
</Warning>

Embedding keeps the payer on your site. Doing that alone works fine — the page
can connect its own wallet.

To match the page to your brand, pass [theme parameters](/sdk/theming) on the
URL — colours, font, and corner radius, no CSS.

## Bridging a connected wallet

If your app has **already** connected a wallet, making the payer connect a
second time inside the iframe is a real drop-off point. The parent bridge lends
your existing connection to the frame: the iframe asks for signatures, your
provider produces them, and the payer never reconnects.

```typescript theme={"system"}
import { createParentBridge } from "@hypermid/sdk";

// The iframe must be pointed at `embedUrl`. The bridge cannot help a frame
// that was refused before it loaded.
const bridge = createParentBridge({
  iframe: document.getElementById("hypermid") as HTMLIFrameElement,
  provider: walletClient.transport,   // any EIP-1193 provider
  address: connectedAddress,
  chainId: currentChainId,

  onReady: () => setFrameReady(true),
  onPaymentComplete: (paymentId, txHash, paidAmount) => {
    // The payer's transaction is in. Show a confirmation — but treat your
    // webhook as the trigger to release goods.
    showPending(txHash);
  },
  onError: (paymentId, reason) => showError(reason),
});

bridge.start();
```

Call `bridge.stop()` when the iframe unmounts, or the listener outlives it.

<Warning>
  `onPaymentComplete` means the payer **submitted** a transaction — not that
  funds have settled. Cross-chain routes still have a bridge leg to finish.
  Release goods on the [webhook](/guides/webhooks) plus a session read, never on
  this callback. It is a UI signal, not a settlement signal.
</Warning>

## Keeping the bridge in sync

The bridge is constructed with an address and a chain id. If your user switches
either, tear the bridge down and start a new one — otherwise the frame keeps
signing against the wallet state you handed it at mount:

```typescript theme={"system"}
useEffect(() => {
  const bridge = createParentBridge({ iframe, provider, address, chainId, … });
  bridge.start();
  return () => bridge.stop();
}, [address, chainId]);
```

## Security

Messages are origin-checked in both directions — the parent validates the
frame's origin against an allowlist before acting on anything it says, and the
iframe does the same for the parent. That is why `apiBase` has a hard-coded
default: a bridge pointed at an arbitrary origin is a phishing surface, so
overriding it is deliberate rather than convenient.

Never pass a secret key here. The bridge is browser code; sessions are created
on your [server](/sdk/payments).
