> ## 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.

# Customization

> Every knob the SDK exposes — theming, the embedded widget, client options, and webhook verification.

The SDK is configured in four places, each with a different job. This page is
the index of what you can tune; the deep dives live on their own pages.

<Note>
  A live **playground** for previewing these settings is coming. Until then, this
  page is the reference — every option here is real and current.
</Note>

## 1. Look and feel — theming

How the hosted payment page renders: colours, font, and corner radius. Set as
query parameters on the embed (or redirect) URL — no CSS, no build step.

| Knob                                    | Values                                             |
| --------------------------------------- | -------------------------------------------------- |
| `theme`                                 | `light` \| `dark` base preset                      |
| `accent`                                | accent / primary colour (hex, no `#`)              |
| `bgPage`, `bgCard`, `border`            | surface colours                                    |
| `textPrimary`, `textMuted`, `textFaint` | text ramp                                          |
| `font`                                  | family name (best-effort system font unless Inter) |
| `radius`                                | card corner radius `0`–`24`; button radius derived |

Full table, validation rules, and a worked example: **[Theming](/sdk/theming)**.

## 2. Presentation — redirect vs embed

A created session returns **two** URLs, and they are not interchangeable:

| Field      | Use it to                                                                            |
| ---------- | ------------------------------------------------------------------------------------ |
| `url`      | **Redirect** the payer (`/pay/:id`). Not framable — answers `X-Frame-Options: DENY`. |
| `embedUrl` | **Embed** in an iframe (`/embed?paymentId=…`). No framing headers.                   |

```ts theme={"system"}
redirect(session.url!);                    // full-page redirect
<iframe src={session.embedUrl!} />         // keep the payer on your site
```

Details, plus the connected-wallet bridge: **[Widget](/sdk/widget)**.

## 3. Widget behaviour — `createParentBridge`

When you embed the widget **and** your app already has a wallet connected, the
parent bridge lends that connection to the iframe so the payer never reconnects.
Its options are the behavioural hooks:

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

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) => showPending(txHash),
  onError: (paymentId, reason) => showError(reason),
});
bridge.start();
```

| Option              | Type                                      | Notes                                                                      |
| ------------------- | ----------------------------------------- | -------------------------------------------------------------------------- |
| `iframe`            | `HTMLIFrameElement`                       | The frame to bridge. Point it at `embedUrl`.                               |
| `provider`          | EIP-1193 provider                         | From wagmi, Privy, viem, etc.                                              |
| `address`           | `0x${string}`                             | Currently connected address.                                               |
| `chainId`           | `number`                                  | Currently connected chain.                                                 |
| `onReady`           | `() => void`                              | Frame is ready to receive the connection.                                  |
| `onPaymentComplete` | `(paymentId, txHash, paidAmount) => void` | Payer **submitted** a tx — a UI signal, not settlement.                    |
| `onError`           | `(paymentId, reason) => void`             | The frame reported a problem.                                              |
| `apiBase`           | `string`                                  | Hardcoded to production by default; overriding is deliberate, not routine. |

Keep it in sync with the wallet — tear the bridge down and start a new one when
`address` or `chainId` changes, and call `bridge.sendAddressChanged(address,
chainId)` / `bridge.sendDisconnected()` on wallet events. Call `bridge.stop()`
on unmount.

<Warning>
  `onPaymentComplete` fires when the payer **submits** a transaction, not when
  funds settle — cross-chain routes still have a bridge leg. Release goods on the
  [webhook](/guides/webhooks) plus a session read, never on this callback.
</Warning>

## 4. Client options

### Payment client — `PaymentsClientOptions`

Second argument to `createCheckout` / `createDeposit` / `createWithdrawal` /
`getPayment`. Server-side only.

| Option      | Type          | Default                      | Notes                                                     |
| ----------- | ------------- | ---------------------------- | --------------------------------------------------------- |
| `secretKey` | `string`      | —                            | `sk_live_…` or `sk_test_…`. Required. Never in a browser. |
| `apiBase`   | `string`      | `https://server.hypermid.io` | Override the host.                                        |
| `signal`    | `AbortSignal` | —                            | For timeouts and cancellation.                            |

```ts theme={"system"}
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);   // 10s ceiling

const session = await createCheckout(req, {
  secretKey: process.env.HYPERMID_SECRET_KEY!,
  signal: controller.signal,
});
```

### Read client — `new Hypermid(config)`

For chains, tokens, quotes, status, and balances. Safe anywhere; the key is
optional.

| Option    | Type           | Default                      | Notes                                                                |
| --------- | -------------- | ---------------------------- | -------------------------------------------------------------------- |
| `apiKey`  | `string`       | — (anonymous)                | Publishable key, sent as `X-API-Key`. **Not** from a browser bundle. |
| `baseUrl` | `string`       | `https://server.hypermid.io` | Override the host.                                                   |
| `timeout` | `number` (ms)  | `30000`                      | Per-request timeout.                                                 |
| `fetch`   | `typeof fetch` | global `fetch`               | Custom fetch, already bound.                                         |

```ts theme={"system"}
import { Hypermid } from "@hypermid/sdk";

const hypermid = new Hypermid({
  apiKey: process.env.HYPERMID_PUBLISHABLE_KEY,
  timeout: 8_000,
});
```

More: **[Read client](/sdk/read-client)**.

## 5. Webhook verification — `verifyWebhook`

Handling the events Hypermid sends you. Verify against the **raw** body, before
any JSON parsing.

| Option             | Type           | Default    | Notes                                                                |
| ------------------ | -------------- | ---------- | -------------------------------------------------------------------- |
| `toleranceSeconds` | `number`       | `300`      | Replay window. A verbatim delivery is accepted until it is this old. |
| `now`              | `() => number` | `Date.now` | Clock override. For tests only.                                      |

```ts theme={"system"}
import { verifyWebhook } from "@hypermid/sdk/webhooks";

const raw = await req.text();                       // RAW body, not parsed
const ok = verifyWebhook(raw, req.headers.get("x-hypermid-signature-v2"), secret, {
  toleranceSeconds: 120,                            // tighter replay window
});
if (!ok) return new Response("bad signature", { status: 401 });
```

Rotation-safe by design: during a secret rotation two signatures are sent and
either verifies, so in-flight retries don't fail. Full flow:
**[Webhooks](/guides/webhooks)**.

## Sandbox

Every surface toggles to sandbox by the **key**, not a flag or a different host.
Use an `sk_test_…` secret key (payments) or a test publishable key (read
client); everything else is identical, testnets only. See
[Environments](/environments).
