# Headless / API

The funding session API is the substrate under the modal and the WebView. 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.

## Endpoints

| Method | Endpoint | Purpose |
| --- | --- | --- |
| `POST` | `/v2/funding/sessions` | Create a session (optionally with a `paymentMethod` inline). |
| `POST` | `/v2/funding/sessions/{id}/payment_methods` | Commit a source and mint the deposit address. |
| `GET` | `/v2/funding/sessions/{id}` | Read the session and its current status. |
| `GET` | `/v2/funding/chains` | List the routable chains and currencies. |
| `POST` | `/v2/funding/pay_link` | Resolve a prefilled exchange on-ramp URL. |

## Using the SDK

`@openfort/openfort-js` exposes funding under `openfort.funding` — usable from any JavaScript runtime (browser or server) with your publishable key.

When the source route is known upfront, funding is **one call plus a wait** — pass `paymentMethod` at creation and the session comes back with the deposit address. The SDK remembers each session's `clientSecret`, so follow-up calls don't need to thread it.

```ts
import { Openfort } from '@openfort/openfort-js'

const openfort = new Openfort({
  baseConfiguration: { publishableKey: process.env.OPENFORT_PUBLISHABLE_KEY! },
})

// One call: create the session AND mint the deposit address.
// Destination: USDC on Base. Source: USDC on Polygon.
const session = await openfort.funding.sessions.create({
  target: {
    chain: 'eip155:8453',
    currency: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    address: '0xUserWalletAddress',
  },
  paymentMethod: {
    type: 'evm',
    source: { chain: 'eip155:137', currency: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', amount: '10000000' },
  },
})

console.log(session.status)                          // "waiting_payment"
console.log(session.paymentMethod?.receiverAddress)  // address the user sends to
console.log(session.paymentMethod?.addressUri)       // EIP-681 / Solana Pay URI for a QR

// Wait for settlement (polls until succeeded | bounced | expired).
const settled = await openfort.funding.sessions.wait(session.id)
console.log(settled.status) // "succeeded"
```

Prefer the two-step flow when the user picks the source later — `sessions.create({ target })` then `sessions.setPaymentMethod(session.id, { paymentMethod })`; the SDK fills in the remembered `clientSecret` (pass it explicitly for sessions created elsewhere).

:::note
A `@openfort/openfort-node` server namespace is planned; until then, server-side integrations call the REST endpoints below directly.
:::

## REST endpoints

The same three calls over HTTP. Both browser and server calls authenticate with your **publishable key** plus the session `clientSecret` (the secret guards reads as defense-in-depth). A secret-key server flow — create a session for any user, list sessions — is planned but not yet available.

```bash
# Create a session
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…" } }'

# Set a payment method (mints the deposit address)
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 it back
curl "https://api.openfort.io/v2/funding/sessions/{id}?clientSecret=…" \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY"
```

## Payment method types

`setPaymentMethod` (or the inline `paymentMethod` on `create`) takes one of three source types:

:::code-group

```ts [EVM]
{ type: 'evm', source: { chain: 'eip155:137', currency: '0x3c49…3359', amount: '10000000' } }
```

```ts [Solana]
{ type: 'solana', source: { chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', currency: 'EPjF…Dt1v', amount: '10000000' } }
```

```ts [Exchange (CEX)]
{ type: 'cex', cex: 'binance', source: { chain: 'eip155:8453', currency: '0x8335…2913', amount: '10000000' } }
```

:::

* **`evm` / `solana`** — self-custody transfers. The response carries `receiverAddress`, `addressUri` (for a QR), and wallet `deeplinks`.
* **`cex`** — a guided withdrawal from a centralized exchange. The response adds a `cex` object (`network`, `minWithdrawal`, `requiresMemo`) and no deeplinks (exchanges don't expose withdrawal links); the user withdraws to `receiverAddress` themselves.

All three are typed in the SDK (`openfort.funding`) and accepted by the REST endpoint.

:::tip
To let a user **buy** crypto on an exchange rather than transfer, call [`pay_link`](#endpoints) (`openfort.funding.payLink`) — it returns a prefilled Coinbase Onramp / Binance Connect URL that settles straight to the wallet. That's a separate rail, not a session payment method.
:::

:::note
**Fiat (card / Apple Pay) isn't available headlessly yet** — it's a React-modal-only method (see [React modal](/docs/configuration/funding/react)). Headless callers fund via the crypto and exchange rails above.
:::

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

:::note
`refundTo` (on `paymentMethod`) defaults to the target address, which is only valid when source and target share an address family. **Cross-family routes (e.g. Solana → Base) require an explicit `refundTo`** — refunds are sent on the source chain.
:::

## Supported chains & currencies

The full set of routable chains and currencies is sourced live from the rail — fetch it instead of hardcoding (the React pickers use the same endpoint):

```bash
curl https://api.openfort.io/v2/funding/chains \
  -H "Authorization: Bearer $OPENFORT_PUBLISHABLE_KEY"
# → { "chains": [ { "id": "eip155:8453", "name": "Base", "vmType": "evm",
#       "currencies": [ { "symbol": "USDC", "address": "0x8335…2913", "decimals": 6, "logo": "…", "native": false }, … ] }, … ] }
```

The React modal narrows this list with `uiConfig.funding.sourceChains` / `sourceCurrencies` (see [React modal](/docs/configuration/funding/react#choosing-source-chains--currencies)); a headless caller just picks a `chain` + `currency` from the response. A few common ones to copy-paste:

| 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` |

Use the zero address (`0x0000…0000`) as `currency` for a chain's native asset.

:::tip\[Agent-native]
There is no UI in this path — an agent can fund a wallet by calling `create` + `setPaymentMethod` and watching `get`. When the destination is a **backend wallet**, your agent can act on the funds the moment the session reaches `succeeded`.
:::

:::warning
The deposit address accepts a **variable amount** above a small route minimum (you don't have to send the exact amount you quoted with). Each session mints a fresh address; reuse the same `externalId` to get an idempotent session rather than minting a new one.
:::

## Next steps

* Avoid polling — subscribe to [Webhooks](/docs/configuration/funding/webhooks) for status changes.
* Render the flow for users with the [React modal](/docs/configuration/funding/react) or the [WebView](/docs/configuration/funding/webview).
