> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Headless / API

This is the REST and session-model reference for funding — the substrate under the modal, the WebView, and the SDKs. Call it directly from a backend, a script, or an agent to get a deposit address with no UI. The session model is identical everywhere: create a session for a destination, set a payment method, then read it back until it settles.

:::tip
For SDK-specific integration, use the per-SDK pages instead: the [`useFunding` hook (React)](https://www.openfort.io/docs/products/embedded-wallet/react/wallet/funding), the [wallet modal (React)](https://www.openfort.io/docs/products/embedded-wallet/react/ui/configuration#funding), and [React Native](https://www.openfort.io/docs/products/embedded-wallet/react-native/wallet/funding). This page documents the underlying HTTP API they call.
:::

## Endpoints

Sessions and the crypto rail:

| Method | Endpoint | Purpose |
| --- | --- | --- |
| `POST` | `/v2/funding/sessions` | Create a session (optionally with a `paymentMethod` inline). |
| `POST` | `/v2/funding/sessions/{id}/payment_methods` | Commit a payment method — a crypto source (mints the deposit address) or a fiat onramp method. |
| `GET` | `/v2/funding/sessions/{id}` | Read the session and its current status. |
| `GET` | `/v2/funding/chains` | List the routable chains and currencies (public — no auth). |
| `POST` | `/v2/funding/pay_link` | Resolve a prefilled exchange on-ramp URL. |

The [fiat onramp](#fiat-onramp-flow):

| Method | Endpoint | Purpose |
| --- | --- | --- |
| `GET` | `/v2/funding/onramp/methods` | Resolve fiat methods for a destination **without a session** — the discovery call for rendering options up front. |
| `GET` | `/v2/funding/sessions/{id}/methods` | Resolve which fiat methods this buyer can use (region + destination). |
| `POST` | `/v2/funding/sessions/{id}/quotes` | Quote a fiat method — fees and delivered amount. |
| `POST` | `/v2/funding/sessions/{id}/onramp_checkout` | Redeem the provider checkout secret (`embedded` angle). |
| `POST` | `/v2/funding/onramp/verifications` | Send a wallet-pay OTP (`email` or `sms` channel). |
| `POST` | `/v2/funding/onramp/verifications/{id}/submit` | Submit the OTP code. |
| `GET` | `/v2/funding/onramp/limits` | Wallet-pay purchase limits (minor units — cents). |
| `POST` | `/v2/funding/onramp/limits/upgrade` | Mint a provider-hosted limit-upgrade URL. |
| `POST` | `/v2/funding/onramp/auth_intents` | Mint the auth intent the [embedded element](#embedded-checkout) initializes with. |
| `POST` | `/v2/funding/onramp/auth_intents/{id}/tokens` | Exchange the authenticated intent for its server-side token. |
| `GET` | `/v2/funding/onramp/identity` | The buyer's region, tier, and satisfied identity requirements. |

## Using an SDK

Most integrations don't call these endpoints by hand. For React, see the [`useFunding` hook](https://www.openfort.io/docs/products/embedded-wallet/react/wallet/funding) or the [wallet modal](https://www.openfort.io/docs/products/embedded-wallet/react/ui/configuration#funding); for React Native, see [Funding — React Native](https://www.openfort.io/docs/products/embedded-wallet/react-native/wallet/funding). The rest of this page is the HTTP API those SDKs sit on top of.

## REST API

Every endpoint except `/chains` authenticates with your **publishable key** via `Authorization: Bearer`. The `clientSecret` returned on session creation acts as a secondary guard — pass it on reads and payment-method writes as defense-in-depth.

> **Coming soon:** A secret-key server flow for creating sessions on behalf of any user and listing sessions across your account.

### Create a session

```bash
curl -X POST https://api.openfort.io/v2/funding/sessions \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": {
      "chain": "eip155:8453",
      "currency": "0x8335…2913",
      "address": "0xUser…"
    }
  }'
```

Returns a session object containing an `id` and `clientSecret`. Keep the `clientSecret` — you'll need it for subsequent calls.

### Set a payment method

Calling this endpoint mints the deposit address for the session.

```bash
curl -X POST https://api.openfort.io/v2/funding/sessions/{id}/payment_methods \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "clientSecret": "…",
    "paymentMethod": {
      "type": "evm",
      "source": {
        "chain": "eip155:137",
        "currency": "0x3c49…3359",
        "amount": "10000000"
      }
    }
  }'
```

### Read a session

```bash
curl "https://api.openfort.io/v2/funding/sessions/{id}?clientSecret=…" \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY"
```

***

## Payment method types

Pass a `paymentMethod` object either inline on session create or via the `setPaymentMethod` call. Three types are supported — two crypto sources and the fiat onramp:

:::code-group
### EVM

Fund from any EVM-compatible chain. Specify a CAIP-2 chain ID, ERC-20 contract address, and amount in the token's base unit.

```ts [EVM]
{
  type: 'evm',
  source: {
    chain: 'eip155:137',       // Polygon
    currency: '0x3c49…3359',   // USDC on Polygon
    amount: '10000000'          // 10 USDC (6 decimals)
  }
}
```

### Solana

Fund from Solana mainnet using any SPL token.

```ts [Solana]
{
  type: 'solana',
  source: {
    chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',  // Solana mainnet
    currency: 'EPjF…Dt1v',                               // USDC on Solana
    amount: '10000000'                                    // 10 USDC (6 decimals)
  }
}
```

### Onramp

Buy with fiat — Apple Pay, Google Pay, card, or bank transfer.

```ts [Onramp]
{
  type: 'onramp',
  method: 'card',            // 'apple_pay' | 'google_pay' | 'card' | 'bank_transfer'
  sourceAmount: '25',        // fiat amount, human units
  sourceCurrency: 'USD',     // ISO-4217
  country: 'US',             // optional override; defaults to the request IP
  angles: ['popup'],         // optional client-capability declaration — see the resolve step
  redirectUrl: 'https://yourapp.com/wallet'  // optional post-checkout return
}
```
:::

* **`evm` / `solana`** — self-custody transfers. The response carries `receiverAddress`, `addressUri` (for a QR), and wallet `deeplinks`.
* **`onramp`** — a fiat purchase. The response carries the checkout `url` and an [`angle`](https://www.openfort.io/docs/configuration/funding#fiat-onramp) (`popup` | `native` | `embedded`) telling you how to present it. Resolve the buyer's available methods with [`GET …/methods`](#fiat-onramp-flow) first — committing a method the region doesn't support fails.

Exchange funding is a separate rail — see [`pay_link`](#endpoints) below.

:::warning[Wallet pay needs a verified buyer]
`apple_pay` / `google_pay` on the **native** angle additionally require an OTP-verified buyer identity on the payment method: `email`, `phoneNumber` (US mobile, E.164), `phoneNumberVerifiedAt`, `agreementAcceptedAt`, and the `smsVerificationId` / `emailVerificationId` returned by the [verification endpoints](#wallet-pay-verification). Ignored for `card` / `bank_transfer`.
:::

:::tip[Show fees and the minimum before the user sends]
Once a payment method is set, surface these two fields from `session.paymentMethod`:

* `fees` — array of `{ kind, amount, currency }`, where `kind` is `gas` | `relayerGas` | `relayerService` | `app`.
* `minAmount` — the smallest sendable amount in base units, or `null` when the route has no floor.
:::

## Fiat onramp flow

Fiat funding is the same session with an `onramp` payment method. The extra steps are **resolve** (which methods this buyer can use) and optionally **quote** (what it costs) before the commit.

:::note
Fiat methods are off by default — enable the onramp and pick methods in the dashboard's **Funding** section first. Until then `…/methods` returns no fiat rows. See [Enabling it](https://www.openfort.io/docs/configuration/funding#enabling-it).
:::

:::steps
### Resolve the buyer's methods

```bash
curl "https://api.openfort.io/v2/funding/sessions/{id}/methods?clientSecret=…&country=US" \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY"
```

Returns `{ country, methods }`, where each row is:

| Field | Meaning |
| --- | --- |
| `method` | `apple_pay` | `google_pay` | `card` | `bank_transfer`. |
| `angle` | How to present the checkout — `popup` | `native` | `embedded`. |
| `label` | Server-resolved display text (bank transfer shows the regional rail). |
| `rail?` | `ach` | `sepa` | `interac`, on bank transfers. |
| `requiresDeviceCheck?` | Gate the row on device capability (e.g. Apple Pay). |
| `providerPublishableKey?` | Initializes the provider elements on `embedded` rows. |

* Render **only** what comes back — an absent row can't commit in the buyer's region.
* `country` defaults to the request IP; pass it explicitly when you know the buyer's region (or on localhost, where there's no geo header).
* To render options **before any session exists**, the same resolution is available sessionless: `GET /v2/funding/onramp/methods?targetChain=…&targetCurrency=…&country=US` (plus optional `methods`, a comma-separated allowlist in display order).

#### Declare what your client can execute (`angles`)

Both resolve calls accept an optional `angles` query — a comma-separated list of the [presentation angles](https://www.openfort.io/docs/configuration/funding#fiat-onramp) your client can actually run.

* **Routing degrades instead of failing**: providers whose flow would resolve to an excluded angle are skipped, falling through to the hosted popup checkout.
* **Example**: a React Native app that can only open a browser sends `angles=popup` — without it, a US buyer's card row resolves to `embedded`, which that client can't execute and the commit would refuse.
* **Be consistent**: pass the same declaration on the quote (`"angles": ["popup"]` in the body) and on the commit (`angles` on the `onramp` payment method), so the priced and committed route match what was resolved.
* **Omit it** for no restriction.

### Quote it (optional)

```bash
curl -X POST https://api.openfort.io/v2/funding/sessions/{id}/quotes \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "clientSecret": "…", "method": "card", "sourceAmount": "25", "sourceCurrency": "USD" }'
```

Returns the delivered amount, fees, and exchange rate for the purchase.

### Commit the onramp method

```bash
curl -X POST https://api.openfort.io/v2/funding/sessions/{id}/payment_methods \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "clientSecret": "…",
    "paymentMethod": { "type": "onramp", "method": "card", "sourceAmount": "25", "sourceCurrency": "USD" }
  }'
```

The session comes back in `waiting_payment` with `paymentMethod.url` and `angle`. Present it per the [angle table](https://www.openfort.io/docs/configuration/funding#fiat-onramp) — open `popup` URLs in a popup or new tab, never an iframe.

### Poll until settled

Settlement is driven by provider webhooks server-side. Poll `GET /v2/funding/sessions/{id}` or subscribe to [webhooks](https://www.openfort.io/docs/configuration/webhooks#funding-events) until the session reaches `succeeded`. The checkout window closing is **not** an outcome — the buyer may have paid right before closing.
:::

A session accepts **one** payment method — to retry a failed or abandoned checkout, create a fresh session.

### Wallet pay verification

The native Apple Pay / Google Pay sheet requires an OTP-verified buyer. Openfort issues the codes — one verification per channel (`email` and `sms`; the phone must be a US mobile in E.164):

:::steps
#### Send a code

```bash
curl -X POST https://api.openfort.io/v2/funding/onramp/verifications \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "sms", "destination": "+14155550123" }'
```

Returns `{ "verificationId": "…" }`.

#### Submit the code

```bash
curl -X POST https://api.openfort.io/v2/funding/onramp/verifications/{verificationId}/submit \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "otpCode": "123456" }'
```

Returns `{ "verificationId": "…", "verificationExpiresAt": "…" }`.

#### Attach the identity to the commit

Verify **both** the email and the phone, then pass on the `onramp` payment method: `email`, `phoneNumber`, `phoneNumberVerifiedAt`, `agreementAcceptedAt`, `smsVerificationId`, `emailVerificationId`.
:::

Worth knowing:

* Verifications stay valid for **~60 days** — cache them per buyer to skip the OTPs next time.
* On test keys, sandbox numbers (`+1000` + 7 digits) are accepted.
* Requests are rate-limited per project and per destination.

Wallet pay runs on **guest checkout** with purchase limits:

| Call | Returns |
| --- | --- |
| `GET /v2/funding/onramp/limits?phoneNumber=…&method=apple_pay` | The remaining allowance, **in minor units (cents)**. |
| `POST /v2/funding/onramp/limits/upgrade` | A single-use, provider-hosted upgrade URL. Identity collection happens at the provider — it never passes through Openfort. |

### Embedded checkout

The `embedded` angle keeps the card checkout inside your page instead of a popup. The buyer authenticates and enters card details in the **provider's** elements, which your client mounts with the `providerPublishableKey` from the `embedded` row of `…/methods`; Openfort's endpoints below carry the handshake around them. Card data never touches your code or ours.

:::warning[Use the SDKs for this one]
This is the only funding flow with a client-side provider dependency. The [React wallet modal](https://www.openfort.io/docs/products/embedded-wallet/react/ui/configuration#funding) and the [`useOnramp` hook](https://www.openfort.io/docs/products/embedded-wallet/react/wallet/funding#fiat-checkout-with-useonramp) drive it end to end — the endpoints below are for building the element handshake yourself on a platform we don't ship a hook for.
:::

:::steps
#### Mint an auth intent

```bash
curl -X POST https://api.openfort.io/v2/funding/onramp/auth_intents \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "buyer@example.com" }'
# → { "id": "…" }
```

Mount the provider's auth element with this id. It returns the authenticated buyer's `customerRef` when the buyer completes it. Both this endpoint and the exchange below answer `501`/`400` until the provider's embedded access is configured for the deployment.

#### Exchange the intent for its token

```bash
curl -X POST https://api.openfort.io/v2/funding/onramp/auth_intents/{id}/tokens \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY"
# → { "exchanged": true }
```

Call this once the auth element reports success. The token is stored server-side and never returned to the client — the commit and the checkout redemption below look it up by intent id.

#### Read identity and limits (optional)

```bash [identity]
curl "https://api.openfort.io/v2/funding/onramp/identity?authIntentId=…&customerRef=…" \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY"
```

Returns `{ region, level, providedFields }` — `region` is `eu` | `us` | `null`, `level` is `L0` | `L1` | `L2` | `PENDING` | `REJECTED` | `REQUIRES_KYC`.

```bash [limits]
curl "https://api.openfort.io/v2/funding/onramp/limits?authIntentId=…&walletAddress=…&network=…" \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY"
```

Returns `{ limits, remainingMinor, remainingTransactions, upgrade }`.

* **`identity`** names the steps the buyer has already satisfied, so the element flow asks only for what's outstanding. It mainly matters in the EU, where the tier drives extra sub-steps.
* **`limits`** is the same endpoint the wallet-pay rail uses, keyed by `authIntentId` instead of a phone number. Amounts are in **minor units (cents)**; a `null` limit means the provider gave no answer — treat it as unrestricted, not zero.
* **Both are best-effort** — a failed lookup should degrade to over-asking the buyer, never to a failed purchase.

#### Commit with the embedded block

```bash
curl -X POST https://api.openfort.io/v2/funding/sessions/{id}/payment_methods \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "clientSecret": "…",
    "paymentMethod": {
      "type": "onramp", "method": "card", "sourceAmount": "25", "sourceCurrency": "USD",
      "embedded": { "authIntentId": "…", "customerRef": "…", "paymentToken": "…" }
    }
  }'
```

`paymentToken` comes from the provider's payment element. The commit creates a *headless* provider session, so the response carries `providerSessionId` and a `null` `url` — there is nothing to open.

#### Redeem the checkout secret

```bash
curl -X POST https://api.openfort.io/v2/funding/sessions/{id}/onramp_checkout \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "clientSecret": "…" }'
# → { "clientSecret": "…" }   // the provider's one-shot element secret
```

Hand the returned secret to the provider element's checkout call, keyed by `providerSessionId`.

* The secret is **one-shot** and only valid after an `embedded` commit; mandate acceptance happens server-side on this call.
* Then poll the session as in the [flow above](#fiat-onramp-flow) — the element's own result is a UI signal, not the settlement source of truth.
:::

## Response fields

A `FundingSession` (returned by `create`, `setPaymentMethod`, `get`, and `wait`):

| Field | Description |
| --- | --- |
| `id` | Session identifier. |
| `status` | One of `requires_payment_method` | `waiting_payment` | `processing` | `succeeded` | `bounced` | `expired`. |
| `clientSecret` | Secret that guards reads and `setPaymentMethod`; the SDK remembers it per session. |
| `target` | Destination `{ chain, currency, address }`. |
| `amountUnits` | Locked deposit amount in destination base units, or `null` when the sender chooses. |
| `metadata` | Arbitrary string map stored with the session, or `null`. |
| `externalId` | Idempotency / correlation key, or `null`. |
| `strict` | `true` for a single-use deposit address, `false` for an open reusable one. |
| `paymentMethod` | The committed source route (see below); `null` until a payment method is set. |
| `createdAt` | When the session was created. |
| `expiresAt` | When the session expires. |

The `paymentMethod` — a crypto route (`type` `evm` | `solana`):

| Field | Description |
| --- | --- |
| `source` | Source `{ chain, currency, amount }`. |
| `receiverAddress` | Address the user sends to. |
| `addressUri` | EIP-681 / Solana Pay URI for a QR. |
| `deeplinks` | Wallet deeplinks for the transfer (array). |
| `fees` | Array of `{ kind, amount, currency }` route fees. |
| `minAmount` | Minimum sendable amount in base units, or `null`. |

Or a fiat route (`type` `onramp`):

| Field | Description |
| --- | --- |
| `method` | `apple_pay` | `google_pay` | `card` | `bank_transfer`. |
| `angle` | How to present the checkout: `popup` | `native` | `embedded`. |
| `url` | Hosted checkout URL (`popup`) or in-page payment URL (`native`); `null` on `embedded`. |
| `providerSessionId` | The provider's session id for this commit (embedded flow), or `null`. |
| `fees` | Array of `{ kind, amount, currency }` route fees. |
| `minAmount` | Minimum purchase, or `null`. |

## Session options

`create` accepts a few extras:

| Field | Description |
| --- | --- |
| `paymentMethod` | Set the source route in the same call (one-call funding). The response comes back in `waiting_payment` with the deposit address. |
| `amountUnits` | Lock the deposit to a fixed amount (destination base units). Omit to let the sender choose. |
| `externalId` | Idempotency / correlation key. Reusing it returns the existing session — unchanged, even if `paymentMethod` is supplied (advance it with `setPaymentMethod` + its `clientSecret`). |
| `metadata` | Arbitrary string map stored with the session. |
| `strict` | `true` mints a single-use deposit address; `false` (default) mints an open address reusable for the route. |

## Refunds

If a deposit can't be completed, the session reaches `bounced` and the funds are refunded on the **source** chain (less gas) to `refundTo`. `refundTo` is set on `paymentMethod` and defaults to the target address for same-VM routes; it is **required** for cross-VM routes (e.g. Solana → Base) because the destination address isn't a valid address on the source chain.

:::note
A `bounced` status is terminal for the session — the source funds are returned, not the destination. Set `refundTo` to an address you control on the source chain whenever the source and target are on different VMs.
:::

## Supported chains & currencies

Fetch the full set of routable chains and currencies directly from the rail — don't hardcode them. The React pickers use this same endpoint internally.

```bash
curl "https://api.openfort.io/v2/funding/chains?livemode=true"
```

The endpoint is public (no auth) so a source picker can load before the user signs in. `livemode=false` lists the testnet rail instead.

The React modal filters this list via `uiConfig.funding.sourceChains` / `sourceCurrencies` (see [Wallet Modal configuration](#)). In headless mode, pick any chain + currency from the response directly.

**Common chains — copy-paste ready**

| Chain | CAIP-2 `chain` | USDC `currency` |
|---|---|---|
| Base | `eip155:8453` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| Polygon | `eip155:137` | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` |
| Arbitrum | `eip155:42161` | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` |
| Optimism | `eip155:10` | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` |
| Ethereum | `eip155:1` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` |
| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |

> To fund with a chain's **native asset** (ETH, MATIC, SOL), use `0x0000000000000000000000000000000000000000` as the `currency`.

### Agent-native usage

There's no UI in this path. An agent funds a wallet by calling `create` → `setPaymentMethod` → polling `get` until the session reaches `succeeded`. At that point, if the destination is a backend wallet, your agent can act on the funds immediately.

A few things worth knowing:

* **Variable amounts** — the deposit address accepts any amount above the route minimum. You don't need to send the exact quoted amount.
* **Fresh addresses** — each session mints a new deposit address.
* **Idempotency** — pass the same `externalId` to retrieve an existing session instead of minting a new one.

## Next steps

* Avoid polling — subscribe to [Webhooks](https://www.openfort.io/docs/configuration/webhooks#funding-events) for status changes.
* Render the flow for users with the [React wallet modal](https://www.openfort.io/docs/products/embedded-wallet/react/ui/configuration#funding) or the [hosted deposit page](https://www.openfort.io/docs/configuration/funding#hosted-deposit-page).
