# API reference

## `createYodlSdk(config)`

Returns a `YodlSdk` instance. Call it once for the whole app.

| Option | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `integrationAddress` | `string` | Yes | — | Your integration wallet address. Identifies you to Yodl and attributes payments on chain |
| `supportedChainIds` | `number[]` | Yes | — | Chain IDs the wallet can switch to |
| `yodlUiBaseUrl` | `string` | No | `https://sdk-webview.yodl.me` | Base URL for the hosted Yodl UI |
| `tokenlist` | `YodlTokenInfo[]` | No | Yodl's token list | The tokens your users hold and pay with. See [`tokenlist`](#tokenlist) |

`integrationAddress` must be a non-empty `0x` address. The SDK sends it alongside package metadata on Yodl-owned requests:

```txt
X-Yodl-Integration-Id: 0x000000000000000000000000000000000000dEaD
X-Yodl-SDK-Name: @yodlpay/react-native
X-Yodl-SDK-Version: <installed package version>
```

:::note
Don't confuse this with the `X-Yodl-Integration` header on [payment notifications](/integrations/notifications). That one travels the other way — Yodl signing a webhook to you. This one is your app identifying itself to Yodl.
:::

Instance members:

* **`sdk.config`** — the resolved config, read-only.

### `tokenlist`

By default the hosted UI works from Yodl's token list and offers every listed token the user holds. To use your own list instead, pass its entries. They take the [Token Lists](https://tokenlists.org) `TokenInfo` shape, so entries from `@yodlpay/tokenlists` work as they are:

```ts
const sdk = createYodlSdk({
  integrationAddress: '0x000000000000000000000000000000000000dEaD',
  supportedChainIds: [8453],
  // USDC on Base, and nothing else.
  tokenlist: [
    {
      chainId: 8453,
      address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
      decimals: 6,
      name: 'USD Coin',
      symbol: 'USDC',
      extensions: { coinGeckoId: 'usd-coin', peggedTo: 'USD' },
    },
  ],
});
```

The hosted UI then reads, shows and pays from those tokens alone, including tokens Yodl's list doesn't carry. Anything else the wallet holds is left out of its balances, never offered as a way to pay, and never spent. With a single token, the payer sees a balance in its currency, such as "USD balance", with no token picker and no token names or logos.

A token's USD value comes from `extensions.coinGeckoId`, or from `extensions.peggedTo: 'USD'` for a dollar stablecoin. An entry with neither is valued at zero, so it never covers a payment. A token's `decimals` are read from its contract, not taken from the entry, and an entry whose address holds no token contract on that chain is left out.

A user who holds none of the listed tokens, or not enough of them, is told they don't have enough balance. The hosted UI does not fall back to Yodl's list.

`createYodlSdk` throws if the list is empty or an entry doesn't match `TokenInfo`: a positive integer `chainId`, a `0x` `address`, a non-negative integer `decimals`, string `name` and `symbol`, and optionally a string `logoURI`, string `tags` and a plain `extensions` object of strings, finite numbers, booleans and nulls, whose `coinGeckoId` and `peggedTo` are strings where present. Every entry must be an object: a hole in the array or a `null` is refused, and the error names the entry and the field. Only those fields are sent, so you can pass objects that carry more, such as balances.

The list is read once, by `createYodlSdk`, and `sdk.config.tokenlist` is frozen. A mounted Yodl component keeps the list its page loaded with, so to change the list while the app runs, for example after the user adds a token, create a new SDK with the whole new list and remount the component.

`tokenlist` is not checked against `supportedChainIds`. A token on a chain outside it is accepted, but a payment that needs the wallet to switch to that chain fails, so list only tokens on chains in `supportedChainIds`.

## `isPaymentQr(qrData)`

`string | string[]` → `boolean`. Returns `true` if any entry is a Yodl-supported payment QR.

Pure and standalone: it takes no SDK instance, so you can guard a render before `<YodlProvider>` is mounted.

```tsx
import { isPaymentQr } from '@yodlpay/react-native';

if (!isPaymentQr(scannedQr)) return null;
```

## `<YodlProvider />`

Supplies the SDK instance and wallet state to the components. Render it once, high in your tree.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `sdk` | `YodlSdk` | Yes | Instance from `createYodlSdk()` |
| `provider` | `EIP1193Provider` | Yes | EIP-1193 provider from any wallet SDK (`@yodlpay/sdk-core`'s type: `request` only, events not required; `withYodl7702` accepts and returns the same type, so no cast is needed). Prefer the wallet's native EIP-5792 support; only wrap with `withYodl7702(...)` if it lacks it — see [EIP-7702 provider](/sdk/eip-7702-provider) |
| `address` | `string` | Yes | Connected wallet address |
| `chainId` | `number` | Yes | Chain ID the wallet is currently on |
| `theme` | `YodlTheme` | No | Default hosted-UI palette; all fields optional |
| `themeMode` | `'system' \| 'light' \| 'dark'` | No | Default host-controlled mode |

`address` and `chainId` are the wallet's **live** account and chain — pass them straight from your wallet SDK's hooks (`useWallets()`, `useAccount()`, …), not as fixed values. They tell the hosted UI who is connected and on which chain, and the SDK pushes `accountsChanged` / `chainChanged` into the UI whenever they change. The `provider` is used only as the RPC and signing transport.

## Shared visual props

`<YodlSignup>`, `<YodlPayment>` and `<YodlDashboard>` all accept these, in addition to their own props below.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `theme` | `YodlTheme` | No | Palette override; inherits the provider `theme` when omitted |
| `themeMode` | `'system' \| 'light' \| 'dark'` | No | Mode override; inherits the provider `themeMode` when omitted |
| `backgroundColor` | `string` | No | Colour behind the WebView before first paint; overrides the theme background |
| `renderLoading` | `() => ReactElement \| null` | No | Replaces the loading overlay's contents; `null` suppresses the overlay entirely |
| `onReady` | `() => void` | No | Called once per mount, the first time the hosted UI is revealed |
| `style` | `ViewStyle` | No | Container style |

See [theming](/sdk/react-native/theming) for how overrides resolve, and [loading and readiness](#loading-and-readiness) for the last two.

### Loading and readiness

Until the hosted UI reports it has painted, the SDK covers the WebView with an overlay filled with `backgroundColor` and centred on an `ActivityIndicator`. It exists so the user never sees react-native-webview's hardcoded-white loading view, or the document's own white before its CSS lands.

`renderLoading` and `onReady` are two ways to take that over, and they answer different questions. **Pick one.** Using both is how you end up with the two loaders this exists to prevent.

#### Recipe A — Yodl's overlay, your loader

The SDK still owns the cover; you supply what goes inside it. Nothing in your own tree needs to know when the hosted UI is up, so `onReady` has no job here.

```tsx
<YodlPayment
  qrData={qrData}
  // Painted behind the WebView and in the overlay, so there is one ground colour.
  backgroundColor="#18181b"
  renderLoading={() => <MyBrandLoader />}
/>
```

`renderLoading` replaces what goes inside the overlay:

| Return value | Result |
| --- | --- |
| An element | Rendered centred on `backgroundColor`. The wrapper stays, so there is still no white flash |
| `null` | **No overlay at all** — no view is mounted, so nothing sits over the WebView intercepting touches. `backgroundColor` is still painted behind it |
| Prop omitted | The default `ActivityIndicator`, in the theme's `textSecondary` |

It is called on every render while the UI is still loading, so keep it cheap and start no work inside it.

#### Recipe B — your own screen covers it

Your app is already showing a full-screen loading state over the component. Suppress Yodl's overlay so there is only one, and let `onReady` tell you when to take yours down.

```tsx
<>
  {!ready && <MyLoadingScreen />}
  <YodlPayment
    qrData={qrData}
    backgroundColor="#18181b"
    renderLoading={() => null}
    onReady={() => setReady(true)}
  />
</>
```

`onReady` is the only way to do this: `renderLoading` is called *during render*, so you cannot `setState` from it.

`onReady` fires **once per mount**, the first time the UI is revealed. It does not fire again. The hosted document re-posts readiness when it reloads, but the overlay has already lifted and does not come back, so there is nothing to re-announce — it lifts once and stays lifted.

:::note
**The reveal has an 8-second ceiling.** If the hosted UI never reports readiness, the SDK reveals anyway and fires `onReady`, rather than leaving an overlay up forever.

It is a last resort for a page that is genuinely dead, not a routine path. A reveal on that timer arrives *without* the readiness handshake, so the hosted UI has received neither your theme nor the SDK's capability announcement. A current hosted build runs its own, shorter ceiling and always reports readiness first, so you only reach 8 seconds when the page never loaded at all. The ceiling reveals — it does not report an error. A page that failed to load reaches `onError` as a [`YodlWebViewError`](#errors).
:::

## `<YodlSignup />`

Renders the hosted signup flow as a full-screen component inside `<YodlProvider>`. Also accepts the [shared visual props](#shared-visual-props).

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `onStatusChange` | `(status: 'checking' \| 'signed_out' \| 'signing_up' \| 'signed_up') => void` | No | Called when the hosted flow reports signup status |
| `onSignedUp` | `() => void` | No | Called when signup completes |
| `onDismiss` | `() => void` | No | Called when the hosted success screen is dismissed |
| `onError` | `(error: Error) => void` | No | Called on SIWE, registration, verification, RPC or WebView errors. See [errors](#errors) |
| `fetchEmailAttestation` | `(request: YodlEmailAttestationRequest) => Promise<YodlEmailAttestation \| null>` | No | Supplies an [email attestation](/integrations/email-attestation) so the user skips Yodl's OTP |

### `fetchEmailAttestation`

Called once per registration attempt, at the moment the hosted flow is about to create the account — after the wallet signature, before the email is submitted. Return `null` to let the user verify by code as usual.

Passing the prop is also how the hosted page learns, before it loads, that your app can attest. A sign-in that finishes before the page has painted anything still asks for an attestation instead of falling back to the email code.

The request carries the wallet and nothing else. Your backend decides which address to attest — from its own session, never from anything the hosted page sent — and the `email` you return is the one the Yodl account is created with, so return it every time. A token with no `email` is treated as a decline and the user gets the OTP.

```ts
interface YodlEmailAttestationRequest {
  // The wallet the hosted flow proved with SIWE. Mint `sub` for exactly this.
  address: `0x${string}`;
  // Aborted on timeout, unmount, or session change. Forward it into your fetch.
  signal: AbortSignal;
}

interface YodlEmailAttestation {
  token: string; // the compact JWS your backend signed
  email?: string; // the token's `email` claim — the address you verified, required in practice
}
```

| Behaviour | Result |
| --- | --- |
| Resolves a `token` and an `email` | Sent to Yodl; the OTP is skipped if it verifies |
| Resolves a `token` with no `email` | Treated as a decline, normal OTP flow |
| Resolves `null` | Normal OTP flow. **Not** reported through `onError` |
| Throws | Normal OTP flow |
| Takes longer than 30 seconds | Aborted via `signal`, normal OTP flow |
| Prop omitted | Normal OTP flow, with no added latency |

:::warning
Mint a fresh token on every call. The token is single use and lives at most 5 minutes, so a cached one fails — and once its `jti` is spent, replaying it fails permanently rather than falling back cleanly on the next attempt.
:::

Yodl calls this at most 3 times per hosted session, and only when a flow actually registers an account. `<YodlPayment />` and `<YodlDashboard />` take the prop as well: a wallet with no Yodl account is asked to register inside the payment flow, and that registration skips the OTP on the same terms. Omit the prop on a surface and a registration there falls back to the code flow.

The 30-second budget is the same one the SDK gives its own network calls. It is deliberately generous: a mint that is cut short costs the user the wait *and* the OTP, and because attestation failures are silent it would not show up as an error anywhere. Signing itself takes milliseconds, so the budget is really covering the round trip to your backend on a bad mobile connection.

## `<YodlPayment />`

Renders the payment flow for a scanned QR code. Renders nothing if `qrData` is empty or unsupported. Also accepts the [shared visual props](#shared-visual-props).

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `qrData` | `string` | Yes | Raw scanned QR data string |
| `navigateOnSubmit` | `'always' \| 'never'` | No | Whether the hosted UI moves to its own status screen once the transaction is submitted. Defaults to `'always'` |
| `onTransactionSent` | `(txHash: string) => void` | No | Called after a transaction is sent |
| `onPaymentDetails` | `(details: YodlPaymentDetails) => void` | No | Called with payment lifecycle details from the UI |
| `onError` | `(error: Error) => void` | No | Called on RPC, payment or WebView errors. See [errors](#errors) |
| `fetchEmailAttestation` | `(request: YodlEmailAttestationRequest) => Promise<YodlEmailAttestation \| null>` | No | Supplies an [email attestation](#fetchemailattestation) if the flow asks a wallet without a Yodl account to register |

### `navigateOnSubmit`

By default the hosted UI moves itself to its payment-status screen the moment the transaction is **submitted** — not when it settles. If your app also transitions on `onTransactionSent`, the user sees two root transitions: the hosted one, then yours.

`'never'` hands that decision to you. The hosted UI stays on the payment screen exactly as it was at the hold: the control stays busy, nothing else is drawn, and its URL does not change. It does keep polling from that screen, so `onPaymentDetails` reports the payment through to `'success'` or `'failure'` exactly as it does under `'always'`.

```tsx
<YodlPayment
  qrData={qrData}
  navigateOnSubmit="never"
  onTransactionSent={(txHash) => router.replace(`/receipt/${txHash}`)}
/>
```

:::warning
Take this only if you actually navigate. A host that sets `'never'` and ignores both `onTransactionSent` and `onPaymentDetails` leaves the user on a busy payment screen with no way forward — which is why the default is `'always'` and omitting the prop is exactly today's behaviour.
:::

The prop feeds the hosted UI's URL, and the WebView is keyed by that URL, so changing the value on a mounted component remounts and reloads it. Pass a constant, not something that flips mid-flow.

The hosted UI reads the parameter once, on its first load, and removes it from the URL. A navigation trace shows it on the entry URL only.

An older hosted build silently drops the parameter and navigates as it always has, so the prop degrades rather than breaking.

## `<YodlDashboard />`

Renders the full Yodl account dashboard. Also accepts the [shared visual props](#shared-visual-props).

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `onTransactionSent` | `(txHash: string) => void` | No | Called after a transaction is sent |
| `onPaymentDetails` | `(details: YodlPaymentDetails) => void` | No | Called with payment lifecycle details from the UI |
| `onError` | `(error: Error) => void` | No | Called on RPC or WebView errors. See [errors](#errors) |
| `fetchEmailAttestation` | `(request: YodlEmailAttestationRequest) => Promise<YodlEmailAttestation \| null>` | No | Supplies an [email attestation](#fetchemailattestation) if the flow asks a wallet without a Yodl account to register |

## Errors

`onError` receives one of three kinds of error. Each is a named class, so an error tracker titles it by name rather than as a plain `Error`:

| Error | When | Fields |
| --- | --- | --- |
| `YodlUIError` | The hosted UI reported a failure: a payment it cannot follow, or a signup step that failed | `code`: the hosted UI's own code, when it sent one |
| `YodlWebViewError` | The WebView under the hosted UI failed, so the page could not report anything itself | `code`, `url`, `status` |
| The wallet's own error | A request to your EIP-1193 provider failed. A user rejection is not reported | Whatever your provider threw |

`YodlWebViewError.code` says what failed:

| `code` | Meaning |
| --- | --- |
| `'load_failed'` | The page did not load: no network, DNS, TLS. The message carries the platform's description |
| `'http_error'` | The page's document answered with an HTTP error; `status` has it |
| `'process_terminated'` | The OS ended the WebView's web process (Android `onRenderProcessGone`, iOS `onContentProcessDidTerminate`). The page is gone, so remount the component to load it again |

`url` is the page's URL without its query, which carries the scanned QR. Both classes are exported:

```tsx
import { YodlWebViewError } from '@yodlpay/react-native';

onError={(error) => {
  if (error instanceof YodlWebViewError && error.code === 'process_terminated') remount();
  reportError(error);
}}
```

## Payment lifecycle events

`onPaymentDetails` is optional and additive — it does not change `onTransactionSent` or `onError` behaviour.

The hosted UI emits generalized payment lifecycle details once it has payment status, and the SDK forwards them. Normalized fields cover transaction hash, chain, lifecycle status, recipient, invoice, token amounts, refund state and rewards where available. The original payload may be included under `raw`.

Status moves through `'processing'` → `'success' | 'failure'`. Under [`navigateOnSubmit="never"`](#navigateonsubmit) a `'submitted'` message opens the sequence, so hosts on that path see `'submitted'` → `'processing'` → `'success' | 'failure'`.

`'processing'` repeats while the payment settles — it is a poll, not a single transition. Treat `onPaymentDetails` as idempotent, and be careful about navigating from inside it.

`onError` during this sequence means the hosted UI has given up on the receipt: an expired session or a backend failure it cannot poll through. The 404 the backend answers while it is still indexing a fresh hash is not one of these. The poll expects it and keeps asking, and the details stream is unaffected. An older hosted build forwarded that 404 as `Payment not found for txHash=…` between `'submitted'` and `'processing'`; a host that still sees it can ignore it, since the terminal status arrives regardless.

### `source`

`details.source` says where the payload's figures came from, which `status` does not tell you:

| Value | Meaning |
| --- | --- |
| `'optimistic'` | Yodl's backend has not indexed the transaction yet. Every figure is the paying device's own pre-submit data. |
| `'indexed'` | Yodl's backend payment record is behind the payload. |

The two disagree on `invoice`. An optimistic payload reports the **settlement** amount and currency there (e.g. `0.78` `USD`); an indexed one reports the **local fiat** amount and currency (e.g. `20000` `VND`) and moves settlement into `settlementAmount`. Render amounts from an optimistic payload and you will show the wrong currency.

If you display figures rather than just tracking progress, wait for `'indexed'`:

```tsx
onPaymentDetails={(details) => {
  setStatus(details.status);
  if (details.source === 'indexed') setAmount(details.payment?.invoice);
}}
```

The two are independent — a payload can be `'indexed'` while `status` is still `'processing'`.

:::note
`source` is absent on hosted builds older than the one that introduced it, so treat a missing value as `'optimistic'`. Some fields — the recipient in particular — can still carry device data on an early `'indexed'` payload while the payment is settling.
:::

:::note
The SDK never fetches transaction details itself — it only bridges messages from the UI. If you need authoritative payment records on your own backend, use [payment notifications](/integrations/notifications) or the [Payments API](/integrations/payments-api).
:::
