# Deposit to Hyperliquid
Source: https://docs.daimo.com/advanced/hyperliquid
Accept payments from any chain and deposit directly to a Hyperliquid account.
Daimo Pay can bridge funds from any chain to a Hyperliquid (Hypercore) account in one session. It uses a [final-call adapter](/advanced/sessions#contract-calls-calldata) on HyperEVM that forwards USDC into Hypercore via Hyperliquid's `CoreDepositWallet`.
## Quick start
```typescript theme={null}
import { hyperEvmUSDC } from "@daimo/sdk/common";
import { encodeFunctionData } from "viem";
const HYPERCORE_DEPOSIT_ADAPTER = "0x3Df610B9168472EfC3CD37ed5005c0e78946c308";
const calldata = encodeFunctionData({
abi: [
{
name: "deposit",
type: "function",
inputs: [
{ name: "recipient", type: "address" },
{ name: "destinationDex", type: "uint32" },
],
outputs: [],
stateMutability: "nonpayable",
},
],
functionName: "deposit",
args: [
"0xRecipientAddress", // Hypercore recipient
0, // 0 = perps, 0xFFFFFFFF = spot
],
});
const res = await fetch("https://api.daimo.com/v1/sessions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_API_KEY",
},
body: JSON.stringify({
destination: {
type: "evm",
address: HYPERCORE_DEPOSIT_ADAPTER,
chainId: hyperEvmUSDC.chainId,
tokenAddress: hyperEvmUSDC.token,
amountUnits: "10",
calldata,
},
display: { title: "Deposit to Hyperliquid", verb: "Deposit" }
}),
});
```
## Destination DEX
The `destinationDex` parameter controls where funds land on Hypercore.
| DEX | Value | Use case |
| ----- | --------------------------- | -------------------------------- |
| Perps | `0` | Perpetual futures trading margin |
| Spot | `4294967295` (`0xFFFFFFFF`) | Spot trading balances |
## New account fee
Hyperliquid deducts \$1 from the first deposit to a new account, so a \$10 deposit lands as \~\$9 on Hypercore. Factor this into your `amountUnits` if needed.
## Refund address
Always set `refundAddress` when using calldata. If the adapter call reverts, funds go to the refund address and the session status becomes `bounced`.
# KYC Import
Source: https://docs.daimo.com/advanced/kyc-import
Reuse Sumsub KYC from another integration before the user starts a Daimo fiat flow
KYC import lets a user reuse an identity check they've already completed elsewhere. If your product runs Sumsub KYC, or works with a partner who does, share the approved applicant with Daimo before the user enters a fiat flow.
When applicable, this expedites enrollment for a fiat flow without asking users to repeat the same identity check. Daimo imports the applicant into our Sumsub tenant, links it to a Daimo account by verified email, and applies the imported review when the user needs fiat access.
KYC import is enabled per-org. [Contact us](mailto:support@daimo.com) before
using this in production.
## How it works
1. Your Sumsub integration creates a reusable KYC share token for the approved user. See Sumsub's [Reusable KYC via API](https://docs.sumsub.com/docs/reusable-kyc-via-api) docs for token generation.
2. Your server sends the token to Daimo with [`POST /v1/sumsub/import`](/api-reference/import-sumsub-kyc).
3. Daimo imports the applicant and stores the verified email from Sumsub.
4. If a Daimo account with that email exists, Daimo links the import immediately.
5. If the account does not exist yet, Daimo links it when the user signs in with the same email.
Once linked, the user skips repeat KYC in Daimo-hosted fiat flows when the imported review satisfies the rail's requirements.
Learn more about fiat in the [guide](/guides/fiat).
## Import a share token
Call the import endpoint from your backend with your Daimo API key. Never call it from the client.
```bash theme={null}
curl -X POST https://api.daimo.com/v1/sumsub/import \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"shareToken": "SUMSUB_REUSABLE_KYC_SHARE_TOKEN"
}'
```
The response returns a Daimo import ID:
```json theme={null}
{
"kycImport": {
"id": "02ab4563-07ee-4373-8472-9c7dc1027409"
}
}
```
The endpoint is idempotent for the same imported applicant. If the same verified email is already tied to a different Sumsub applicant, Daimo returns `409 conflict`.
## Email matching
The imported Sumsub applicant must include a valid verified email. Daimo normalizes that email and uses it as the link key.
The user should sign in to Daimo with the same email they used for the original KYC. If they use a different email, Daimo cannot attach the imported review to their account, and the hosted fiat flow may require additional identity verification.
## When to call it
Call KYC import as soon as you have the share token. You do not need to wait for a Daimo session or a fiat payment attempt.
Good times to call it:
* After the user completes KYC in your app.
* During account linking, before showing Daimo fiat as a payment option.
* During a migration or backfill for users who already completed KYC elsewhere.
## Reference
* [Import Sumsub KYC](/api-reference/import-sumsub-kyc) - import a reusable KYC share token.
* [Fiat](/guides/fiat) - hosted fiat flows where imported KYC may be used for expedited verification.
* [Custom Integration](/guides/custom-integration) - create sessions and drive hosted payment methods directly.
# Advanced Sessions
Source: https://docs.daimo.com/advanced/sessions
Prefilled amounts, contract calls, and payment options.
Advanced options for creating sessions via `POST /v1/sessions`. See also: [Create Session](/api-reference/create-session).
## Prefilled Amount (`amountUnits`)
Pass `destination.amountUnits` to lock the deposit to an exact amount. The user skips amount selection and proceeds directly to the payment flow.
```bash theme={null}
curl -X POST https://api.daimo.com/v1/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": {
"type": "evm",
"address": "0xYourAddress",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amountUnits": "25.00"
},
"display": {
"title": "Deposit to Acme",
"verb": "Deposit"
}
}'
```
| Field | Type | Description |
| ------------- | ---------------- | ------------------------------------------------------------------------------------------------------ |
| `amountUnits` | string, optional | Fixed amount in token units (e.g. `"25.00"` for \$25 USDC). When omitted, the user chooses the amount. |
If the user pays slightly less or more (due to bridge fees or price changes), the session still completes. Any variance is visible in the session response and webhooks.
## Contract Calls (`calldata`)
Pass `destination.calldata` to execute an arbitrary contract call with deposited funds. When using `calldata`, `destination.address` is the contract being called.
### How it works
1. Daimo delivers the destination token to the contract
2. A token approval of the deposit amount is made to the contract
3. The contract is called with the provided `calldata`
4. If the call succeeds, the session completes normally
5. **If the call reverts, funds are sent to `refundAddress` instead, and the session status becomes `bounced`**
### Contract requirements
Your contract must handle variable input amounts. Specifically, it should:
1. Check the allowance of the destination token from `msg.sender`
2. Use `transferFrom` to pull the full allowance
This is true **even when `amountUnits` is set**. While over- or under-payments are rare with a fixed amount, they cannot be guaranteed to never happen.
```solidity theme={null}
function deposit(address token, uint256 /* amount */, address recipient) external {
uint256 allowance = IERC20(token).allowance(msg.sender, address(this));
require(allowance > 0, "no allowance");
IERC20(token).transferFrom(msg.sender, address(this), allowance);
_processDeposit(token, allowance, recipient);
}
```
### Example: generating calldata with viem
```typescript theme={null}
import { encodeFunctionData } from "viem";
const calldata = encodeFunctionData({
abi: [
{
name: "deposit",
type: "function",
inputs: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "recipient", type: "address" },
],
outputs: [],
stateMutability: "nonpayable",
},
],
functionName: "deposit",
args: [tokenAddress, amount, recipientAddress],
});
```
```bash theme={null}
curl -X POST https://api.daimo.com/v1/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": {
"type": "evm",
"address": "0xYourContract",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amountUnits": "100.00",
"calldata": "0x..."
},
"display": {
"title": "Stake USDC",
"verb": "Stake"
},
"refundAddress": "0xYourRefundAddress"
}'
```
### Error handling
If your contract call reverts:
* The session status becomes `bounced`
* Funds are sent to the `refundAddress` you specified
Always provide a `refundAddress` when using calldata to ensure funds are recoverable.
## Payment Options
Optionally pass `display.paymentOptions` to control which payment methods appear and in what order.
When omitted, all available methods are shown.
### Categories
| Option | Description |
| ---------------- | --------------------------------------------------------------- |
| `"AllFiat"` | All eligible fiat methods configured for the org |
| `"AllAddresses"` | Deposit address options for all supported EVM chains |
| `"AllExchanges"` | All exchange options (Coinbase, Binance, Lemon) |
| `"AllWallets"` | All supported wallet deeplinks (MetaMask, Trust, Phantom, etc.) |
### Fiat
| Option | Description |
| ------------ | -------------------------- |
| `"Interac"` | Interac hosted fiat flow |
| `"ApplePay"` | Apple Pay hosted fiat flow |
| `"ACH"` | ACH hosted fiat flow |
### Deposit Addresses
| Option | Description |
| ------------ | ------------------------------- |
| `"Tron"` | Tron USDT deposit address |
| `"Arbitrum"` | Arbitrum deposit address |
| `"Base"` | Base deposit address |
| `"Optimism"` | Optimism deposit address |
| `"Polygon"` | Polygon deposit address |
| `"Ethereum"` | Ethereum deposit address |
| `"BSC"` | BSC (BNB Chain) deposit address |
### Exchanges
| Option | Description |
| ------------ | --------------- |
| `"Coinbase"` | Coinbase onramp |
| `"Binance"` | Binance Connect |
| `"Lemon"` | Lemon cash-out |
### Cash App
| Option | Description |
| ----------- | ------------------------------ |
| `"CashApp"` | Cash App payment via Lightning |
### Wallets
| Option | Description |
| ------------------------------ | ---------------------------------------------------------------- |
| `"MetaMask"` | MetaMask wallet |
| `"Trust"` | Trust Wallet |
| `"Phantom"` | Phantom wallet |
| `"Rainbow"` | Rainbow wallet |
| `"BaseApp"` | Base (Coinbase Wallet) |
| `"Bitget"` | Bitget wallet |
| `"OKX"` | OKX wallet |
| `"Zerion"` | Zerion wallet |
| `"ConnectedWallet"` | Use already-connected browser wallet (no prompt, errors if none) |
| `"AutoconnectInjectedWallets"` | Auto-connect injected browser wallet |
When `paymentOptions` is omitted, the default is `["AllWallets", "AllExchanges", "AllAddresses"]`. CashApp, Tron, AllFiat, and individual chain addresses must be explicitly included.
**Array order** defines display order. A **single option** skips the selection screen (e.g. `["Lemon"]` goes straight to Lemon).
**Nested arrays** control wallet ordering within a group:
```json theme={null}
{
"paymentOptions": ["AllExchanges", ["MetaMask", "Trust", "Phantom"]]
}
```
This shows exchanges first, then a wallet group with MetaMask, Trust, and Phantom in that order. Nested arrays only support wallets.
```bash theme={null}
curl -X POST https://api.daimo.com/v1/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": {
"type": "evm",
"address": "0xYourAddress",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amountUnits": "10.00"
},
"display": {
"title": "Deposit USDC",
"verb": "Deposit",
"paymentOptions": ["Coinbase", "Binance"]
}
}'
```
# Check session
Source: https://docs.daimo.com/api-reference/check-session
/openapi.json put /v1/sessions/{sessionId}/check
Checks the current status of a session. Optionally pass a txHash to hint that the user has sent a transaction, which can speed up detection.
# Create payment method
Source: https://docs.daimo.com/api-reference/create-payment-method
/openapi.json post /v1/sessions/{sessionId}/paymentMethods
Sets the payment method for a session. Transitions the session from requires_payment_method to waiting_payment and returns payment data for the selected method. EVM and Tron return receiver addresses, Solana returns a serialized transaction to sign, exchanges return a hosted payment URL plus waiting copy, and fiat returns a hosted URL.
# Create session
Source: https://docs.daimo.com/api-reference/create-session
/openapi.json post /v1/sessions
Creates a new deposit session. Returns the session object including a client secret for client-side operations.
# Create webhook endpoint
Source: https://docs.daimo.com/api-reference/create-webhook-endpoint
/openapi.json post /v1/webhooks
Creates a new webhook endpoint. The response includes the full HMAC signing secret (shown only on creation). See [webhook verification](https://docs.daimo.com/guides/webhooks#verify-signatures) for more information.
# Delete webhook endpoint
Source: https://docs.daimo.com/api-reference/delete-webhook-endpoint
/openapi.json delete /v1/webhooks/{webhookId}
Soft-deletes a webhook endpoint. It will no longer receive events.
# Import Sumsub KYC
Source: https://docs.daimo.com/api-reference/import-sumsub-kyc
/openapi.json post /v1/sumsub/import
Imports reusable Sumsub KYC from a share token and links it to the Daimo account with the same verified email.
# List webhook endpoints
Source: https://docs.daimo.com/api-reference/list-webhook-endpoints
/openapi.json get /v1/webhooks
Lists all active webhook endpoints for the authenticated organization. Secrets are redacted.
# API Overview
Source: https://docs.daimo.com/api-reference/overview
Base URL, authentication, errors, and core types
## Base URL
```
https://api.daimo.com
```
## Authentication
Endpoints use Bearer token authentication with your API key:
```
Authorization: Bearer YOUR_API_KEY
```
Some endpoints accept a **client secret** instead, passed in the request body or query string. Each session has its own client secret, returned when you create or retrieve the session. Client secrets are safe for client-side use since they only grant access to their specific session.
| Credential | Format | Use |
| ------------- | ------ | ----------------------------------------------------------- |
| API key | UUID | Server-side. Create sessions, retrieve full details. |
| Client secret | UUID | Client-side, per-session. Set payment method, check status. |
## Error format
All errors return a JSON body:
```json theme={null}
{
"error": {
"type": "validation_error",
"code": "invalid_parameter",
"message": "invalid session create request",
"param": "body"
}
}
```
| Field | Type | Description |
| --------- | --------- | -------------------------------------------------- |
| `type` | `string` | Error category (see below) |
| `code` | `string` | Machine-readable error code |
| `message` | `string` | Human-readable description |
| `param` | `string?` | The parameter that caused the error, if applicable |
### Error types
| Type | Description |
| ----------------------- | --------------------------------------------------------------- |
| `authentication_error` | Missing or invalid credentials |
| `validation_error` | Invalid request body or parameters |
| `invalid_request_error` | Valid request but cannot be processed (e.g. resource not found) |
| `api_error` | Internal server error |
### HTTP status codes
| Code | Meaning |
| ----- | ---------------------------------------- |
| `200` | Success |
| `201` | Created |
| `400` | Bad request, validation error |
| `401` | Unauthorized, missing or invalid API key |
| `404` | Not found, session does not exist |
| `500` | Internal server error |
## Session object
The full session object (returned with API key authentication):
```typescript theme={null}
{
sessionId: string; // 32-character hex ID
status: SessionStatus; // see below
destination: {
type: "evm";
address: string; // checksum-encoded destination address
chainId: number; // e.g. 8453
chainName: string; // e.g. "Base"
tokenAddress: string; // destination token address
tokenSymbol: string; // e.g. "USDC"
amountUnits?: string; // requested amount in token units
calldata?: string; // hex-encoded calldata for contract calls
delivery?: { // set once funds are delivered
txHash: string;
receivedUnits: string;
};
};
display: {
title: string; // e.g. "Deposit to Acme"
verb: string; // e.g. "Deposit"
themeCssUrl?: string; // custom theme CSS URL
};
paymentMethod: PaymentMethod | null;
metadata: Record | null;
clientSecret: string; // per-session client credential
createdAt: number; // unix timestamp (seconds)
expiresAt: number; // unix timestamp (seconds)
}
```
Without an API key, `metadata` and `clientSecret` are omitted.
### SessionStatus
| Value | Description |
| ------------------------- | --------------------------------------------------- |
| `requires_payment_method` | Waiting for user to choose how to deposit |
| `waiting_payment` | Payment method set, waiting for deposit transaction |
| `processing` | Deposit detected, routing funds to destination |
| `succeeded` | Funds delivered to destination |
| `bounced` | Delivery failed, funds returned to refund address |
| `expired` | Session timed out |
### PaymentMethod
Three variants based on the source chain:
**EVM:**
```json theme={null}
{
"type": "evm",
"receiverAddress": "0x...",
"source": {
"address": "0x...",
"chainId": 1,
"chainName": "Ethereum",
"tokenAddress": "0x...",
"tokenSymbol": "USDC",
"sentUnits": "10.00",
"txHash": "0x..."
},
"createdAt": 1700000000
}
```
**Tron:**
```json theme={null}
{
"type": "tron",
"receiverAddress": "T...",
"source": {
"address": "T...",
"chainId": 728126428,
"chainName": "tron",
"tokenAddress": "T...",
"tokenSymbol": "USDT",
"sentUnits": "10.00",
"txHash": "..."
},
"createdAt": 1700000000
}
```
**Solana:**
```json theme={null}
{
"type": "solana",
"source": {
"address": "So1...",
"chainId": 501,
"chainName": "solana",
"tokenAddress": "EPjF...",
"tokenSymbol": "USDC",
"sentUnits": "10.00",
"txHash": "..."
},
"createdAt": 1700000000
}
```
The `source` field is populated once the user's deposit transaction is detected. Before that, only `type`, `receiverAddress` (for EVM/Tron), and `createdAt` are present.
EVM and Tron are receiver-address flows. Solana is a sign-and-send flow: instead of a `receiverAddress`, the response returns a transaction payload for the wallet to execute in one click.
### Create payment method response
When creating a payment method, chain-specific fields are returned alongside the session:
* **Tron:** `tron.receiverAddress` (the Tron USDT address to send to) and `tron.expiresAt`
* **Solana:** `solana.serializedTx` (hex-encoded serialized transaction for the wallet to sign and submit)
* **EVM:** no additional fields; the deposit address is the session's EVM deposit address
# Retrieve session
Source: https://docs.daimo.com/api-reference/retrieve-session
/openapi.json get /v1/sessions/{sessionId}
Retrieves a session by ID. With an API key, returns the full Session (including metadata and clientSecret). Without an API key, returns SessionPublicInfo only.
# Retrieve webhook endpoint
Source: https://docs.daimo.com/api-reference/retrieve-webhook-endpoint
/openapi.json get /v1/webhooks/{webhookId}
Retrieves a single webhook endpoint by ID. Secret is redacted.
# Send test event
Source: https://docs.daimo.com/api-reference/send-test-event
/openapi.json post /v1/webhooks/{webhookId}/test
Sends a test webhook event to the specified endpoint. Useful for verifying your webhook handler.
# Custom Integration
Source: https://docs.daimo.com/guides/custom-integration
Build your own deposit UI with the API
Use the Daimo API directly to build a custom deposit experience. This works with any language or framework, no React required.
## Overview
The deposit flow has four steps:
1. **Create a session**: server-side, with your API key
2. **Choose a payment method**: client-side, with the client secret
3. **Wait for deposit**: show the deposit address, poll for status
4. **Handle completion**: process the result
## Step 1: Create a session
Create a session on your server. Never expose your API key to the client.
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": {
"type": "evm",
"address": "0xYourAddress",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amountUnits": "10.00"
},
"display": {
"title": "Deposit to Acme",
"verb": "Deposit",
"paymentOptions": ["AllFiat"]
}
}'
```
```typescript fetch theme={null}
const response = await fetch("https://api.daimo.com/v1/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DAIMO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: {
type: "evm",
address: "0xYourAddress",
chainId: 8453,
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
amountUnits: "10.00",
},
display: {
title: "Deposit to Acme",
verb: "Deposit",
paymentOptions: ["AllFiat"],
},
}),
});
const { session } = await response.json();
```
Pass `session.clientSecret` and `session.sessionId` to your client.
## Step 2: Choose a payment method
On the client, call the payment methods endpoint. This transitions the session from `requires_payment_method` to `waiting_payment`.
**EVM deposit** (any EVM chain):
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/sessions/{sessionId}/paymentMethods \
-H "Content-Type: application/json" \
-d '{
"clientSecret": "SESSION_CLIENT_SECRET",
"paymentMethod": { "type": "evm" }
}'
```
```typescript SDK theme={null}
import { createDaimoClient } from "@daimo/sdk/client";
const daimo = createDaimoClient({ baseUrl: "https://api.daimo.com" });
const result = await daimo.sessions.paymentMethods.create(sessionId, {
clientSecret: "SESSION_CLIENT_SECRET",
paymentMethod: { type: "evm" },
});
```
The response includes a `receiverAddress` in `result.session.paymentMethod`. Display this EVM address to the user - they send tokens to it from any supported chain.
```json theme={null}
{
"session": {
"status": "waiting_payment",
"paymentMethod": {
"type": "evm",
"receiverAddress": "0x1a2B3c4D5e6F...",
"createdAt": 1700000000
}
}
}
```
**Tron USDT deposit:**
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/sessions/{sessionId}/paymentMethods \
-H "Content-Type: application/json" \
-d '{
"clientSecret": "SESSION_CLIENT_SECRET",
"paymentMethod": { "type": "tron", "amountUsd": 10.0 }
}'
```
```typescript SDK theme={null}
const result = await daimo.sessions.paymentMethods.create(sessionId, {
clientSecret: "SESSION_CLIENT_SECRET",
paymentMethod: { type: "tron", amountUsd: 10.0 },
});
```
The response includes a `tron.receiverAddress` in `result.session.paymentMethod`, a temporary Tron address. Display it to the user so they can send USDT to it.
```json theme={null}
{
"session": {
"status": "waiting_payment",
"paymentMethod": {
"type": "tron",
"receiverAddress": "TXyz1234...",
"createdAt": 1700000000
}
}
}
```
**Solana deposit:**
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/sessions/{sessionId}/paymentMethods \
-H "Content-Type: application/json" \
-d '{
"clientSecret": "SESSION_CLIENT_SECRET",
"paymentMethod": {
"type": "solana",
"walletAddress": "So1ana...",
"inputTokenMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amountUsd": 10.0
}
}'
```
```typescript SDK theme={null}
const result = await daimo.sessions.paymentMethods.create(sessionId, {
clientSecret: "SESSION_CLIENT_SECRET",
paymentMethod: {
type: "solana",
walletAddress: "So1ana...",
inputTokenMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
amountUsd: 10.0,
},
});
```
The response includes `solana.serializedTx`, a hex-encoded serialized Solana transaction for the user's wallet to sign and submit.
This preserves a one-click wallet experience on Solana: swap and burn actions are bundled into one signed transaction. Unlike EVM and Tron, there is no `receiverAddress` for `type: "solana"`.
```json theme={null}
{
"session": {
"status": "waiting_payment",
"paymentMethod": {
"type": "solana",
"createdAt": 1700000000
}
},
"solana": {
"serializedTx": "0xabc123..."
}
}
```
**Fiat:**
The user pays in local currency via one of the fiat rails your org has enabled (e.g. Interac in Canada, ACH or Apple Pay in the US). Daimo hosts the payment and identity-verification flow at `fiat.hostedUrl`; open it in a [WebView](/guides/webview) or new browser tab. The session progresses through `waiting_payment` → `processing` → `succeeded` like any other payment method, and Daimo delivers the stablecoin to your destination once the fiat transfer settles.
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/sessions/{sessionId}/paymentMethods \
-H "Content-Type: application/json" \
-d '{
"clientSecret": "SESSION_CLIENT_SECRET",
"paymentMethod": { "type": "fiat", "fiatMethod": "interac" }
}'
```
```typescript SDK theme={null}
const result = await daimo.sessions.paymentMethods.create(sessionId, {
clientSecret: "SESSION_CLIENT_SECRET",
paymentMethod: { type: "fiat", fiatMethod: "interac" },
});
```
Pass `paymentMethod.fiatMethod` (one of `interac`, `ach`, `sepa`, `apple_pay`) to pin the hosted flow to one rail. If omitted, the hosted page shows every fiat method enabled for the org.
```json theme={null}
{
"session": {
"status": "waiting_payment",
"paymentMethod": {
"type": "fiat",
"fiatMethod": "interac",
"createdAt": 1700000000
}
},
"fiat": {
"hostedUrl": "https://daimo.com/webview?session=...&cs=...",
"fiatMethod": "interac"
}
}
```
The `hostedUrl` is returned only once from the `createPaymentMethod` call — store it on the client. It is not included when retrieving the session later. See the [WebView guide](/guides/webview) for how to load the hosted URL in iOS, Android, and React Native.
Fiat rails are enabled per-org. To request access, [contact us](mailto:support@daimo.com).
## Step 3: Wait for session completion
Poll for status updates to the session after the user has paid.
```bash curl theme={null}
curl -X PUT https://api.daimo.com/v1/sessions/{sessionId}/check \
-H "Content-Type: application/json" \
-d '{ "clientSecret": "SESSION_CLIENT_SECRET" }'
```
```typescript SDK theme={null}
const { session } = await daimo.sessions.check(sessionId, {
clientSecret: "SESSION_CLIENT_SECRET",
});
console.log(session.status); // "waiting_payment", "processing", "succeeded"
```
If you know the user's transaction hash, pass it as `txHash` to speed up detection.
## Step 4: Handle completion
Check for terminal statuses:
* `succeeded` - funds delivered
* `bounced` - delivery failed (e.g. contract call reverted). Funds returned to refund address.
* `expired` - session timed out
In the `succeeded` and `bounced` case, `destination.delivery.txHash` refers to the settlement transaction.
# Fiat
Source: https://docs.daimo.com/guides/fiat
Accept deposits from local bank accounts and payment rails via a Daimo-hosted flow
Fiat lets your users deposit in local currency (CAD, USD) using familiar rails like Interac, ACH, and Apple Pay. Daimo hosts identity verification and the deposit UI, settles the fiat transfer, and delivers the stablecoin to your destination.
Fiat rails are enabled per-org. [Contact us](mailto:support@daimo.com) to
request access and have specific rails enabled.
## Supported rails
| `fiatMethod` | Region | Currency |
| ------------ | ------ | -------- |
| `interac` | Canada | CAD |
| `ach` | US | USD |
| `apple_pay` | US | USD |
## How it works
Fiat options appear alongside your other payment options. When the user picks one, they're handed off to a Daimo-hosted page that walks them through:
1. **Rail selection**: Interac, ACH, Apple Pay, etc. Skipped if you've pinned a rail.
2. **Identity verification**: first-time users complete a short KYC step (name, address, ID). Returning users skip through.
3. **Payment**: the user pays through their chosen rail, e.g. an Interac e-Transfer, ACH debit, Apple Pay charge, etc.
4. **Confirmation**: once the fiat payment clears, the page confirms success and the user returns to your app.
The user pays in their local currency and never touches crypto. Under the hood, Daimo settles the fiat transfer and delivers funds to your destination address on the chain you specified.
## Integration
### Modal SDK
Include the `AllFiat` option in `display.paymentOptions` when [creating the session](/api-reference/create-session). The modal renders the option, drives the hosted flow, and fires `onPaymentCompleted` on delivery.
```typescript theme={null}
await fetch("https://api.daimo.com/v1/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DAIMO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: {
type: "evm",
address: "0xYourAddress",
chainId: 8453,
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
amountUnits: "25.00",
},
display: {
title: "Deposit to Acme",
verb: "Deposit",
paymentOptions: ["AllFiat"],
},
}),
});
```
`"AllFiat"` lists every rail available. To pin the flow to a single rail, use its name (`"Interac"`, `"ACH"`, `"ApplePay"`) instead. See [Payment Options](/advanced/sessions#fiat) for the full reference.
| Input | Hosted page behavior |
| ---------------------------------------------- | ------------------------------------- |
| `["AllFiat"]` | Lists every enabled rail for the user |
| `["Interac"]` (or `"ACH"`, `"ApplePay"`, etc.) | Jumps straight into that rail |
### Custom integration
If you're not using the modal, drive the flow yourself: create the session, then call [`POST /v1/sessions/{id}/paymentMethods`](/api-reference/create-payment-method) with `{ type: "fiat" }` to get back a `fiat.hostedUrl`.
[Render that URL in a WebView, iframe, or new tab](#rendering-the-hosted-url). The hosted page handles KYC and payment collection, and Daimo delivers the stablecoin once the fiat transfer clears. See the full four-step flow for [custom integrations](/guides/custom-integration).
Pass the `clientSecret` returned by [`POST /v1/sessions`](/api-reference/create-session).
`fiat.hostedUrl` is returned **only once** from `POST /paymentMethods`. It is
**not** returned from `GET /v1/sessions/{id}`. Store it on the client as soon
as you receive it.
Here is the fiat-specific [`POST /paymentMethods`](/api-reference/create-payment-method) request and response:
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/sessions/{sessionId}/paymentMethods \
-H "Content-Type: application/json" \
-d '{
"clientSecret": "SESSION_CLIENT_SECRET",
"paymentMethod": { "type": "fiat", "fiatMethod": "interac" }
}'
```
```typescript SDK theme={null}
const result = await daimo.sessions.paymentMethods.create(sessionId, {
clientSecret: "SESSION_CLIENT_SECRET",
paymentMethod: { type: "fiat", fiatMethod: "interac" },
});
```
The response:
```json theme={null}
{
"session": {
"status": "waiting_payment",
"paymentMethod": {
"type": "fiat",
"fiatMethod": "interac",
"createdAt": 1700000000
}
},
"fiat": {
"hostedUrl": "https://daimo.com/webview?session=...&cs=...",
"fiatMethod": "interac"
}
}
```
Open `fiat.hostedUrl` in a WebView or a new browser tab. When the user finishes, they return to your app; poll the session or use [webhooks](/guides/webhooks) for the final status.
Omit `fiatMethod` to let the user pick from every rail available, or set it to pin the flow to a specific one.
| Input | Hosted page behavior |
| ------------------------------------- | --------------------------------------- |
| `{ type: "fiat" }` | Lists every rail available for the user |
| `{ type: "fiat", fiatMethod: "ach" }` | Jumps straight into the ACH flow |
## Rendering the hosted URL
`hostedUrl` points to a mobile-friendly Daimo page. Three ways to render it:
* **Native iOS / Android / React Native app** → load in a WebView. The page posts session events back via `postMessage`. Full reference and code samples in the [WebView guide](/guides/webview).
* **Web app** → open in a new tab or redirect. No special integration needed.
* **In-page iframe** → append `?layout=embed` to render inline instead of as a modal.
## Tracking status
Fiat sessions use the same lifecycle, statuses, and webhooks as every other session. For a full overview, see [Sessions](/guides/sessions#session-lifecycle) and [Webhooks](/guides/webhooks).
| Session status | What it means for fiat |
| ----------------- | ----------------------------------------------------------------------- |
| `waiting_payment` | Hosted URL is live; waiting for the user to complete the fiat transfer |
| `processing` | Fiat payment confirmed; Daimo is delivering the stablecoin on-chain |
| `succeeded` | Stablecoin delivered to the destination address |
| `bounced` | On-chain delivery reverted (e.g. contract call failure); funds refunded |
| `expired` | User didn't complete the fiat transfer in time |
Subscribe to `session.processing`, `session.succeeded`, and `session.bounced` via [webhooks](/guides/webhooks) to drive order fulfillment.
## Reference
* [Create Session](/api-reference/create-session) — include `"AllFiat"` or a specific rail in `display.paymentOptions`.
* [Create Payment Method](/api-reference/create-payment-method) — request body `{ type: "fiat", fiatMethod? }`, response `fiat.hostedUrl`.
* [Payment Options](/advanced/sessions#payment-options) — full list of `paymentOptions` values.
* [WebView](/guides/webview) — load the hosted URL in iOS, Android, and React Native.
* [Sessions](/guides/sessions#fiat) — payment method shape and lifecycle details.
# Modal
Source: https://docs.daimo.com/guides/modal
Pre-built React deposit UI
The Daimo modal is a drop-in React component that handles the entire deposit flow: chain selection, wallet connection, transaction signing, and status updates.
## Installation
```bash theme={null}
npm install @daimo/sdk
```
Peer dependencies: `react >= 18`.
## Setup
Wrap your app in `DaimoSDKProvider`:
```tsx theme={null}
import { DaimoSDKProvider } from "@daimo/sdk/web";
import "@daimo/sdk/web/theme.css";
function App() {
return {/* your app */};
}
```
## Basic usage
1. Create a session on your server (see [Quickstart](/quickstart))
2. Pass `sessionId` and `clientSecret` to ``
```tsx theme={null}
import { DaimoModal } from "@daimo/sdk/web";
function DepositPage({ sessionId, clientSecret }) {
return (
console.log("deposit initiated")}
onPaymentCompleted={() => console.log("deposit succeeded")}
/>
);
}
```
The `sessionId` and `clientSecret` come from the session object returned by `POST /v1/sessions`.
## Props
| Prop | Type | Default | Description |
| -------------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionId` | `string` | - | Unique session ID. Sessions are created server-side. (required) |
| `clientSecret` | `string` | - | Unique client secret, returned at session creation. (required) |
| `defaultOpen` | `boolean` | `true` | Whether the modal starts open |
| `connectToInjectedWallets` | `boolean` | `false` | Skip payment method picker. Auto-connect to injected wallets. |
| `connectToAddress` | `Address` | - | Skip payment method picker. Use already-connected EVM wallet at the specified address. |
| `embedded` | `boolean` | `false` | Render inline instead of as a floating modal |
| `platform` | `string` | auto | Caller's platform. Prefer `"desktop"` or `"mobile"`; legacy `"ios"`, `"android"`, and `"other"` still work. Affects wallet and exchange UX. Auto-detected. |
| `returnUrl` | `string` | - | URL to navigate to after successful payment |
| `returnLabel` | `string` | - | Text shown on successful payment. Button label if `returnUrl` is set (default "Return to App"), otherwise text. |
## Event handlers
| Handler | Fires when |
| -------------------- | -------------------------------------- |
| `onPaymentStarted` | User's deposit transaction is detected |
| `onPaymentCompleted` | Funds are delivered to the destination |
| `onOpen` | Modal becomes visible |
| `onClose` | Modal is dismissed |
## Embedded mode
Set `embedded` to render the deposit flow inline instead of as an overlay modal:
```tsx theme={null}
router.push("/success")}
/>
```
This is useful for embedding the deposit flow directly into a page layout.
# Sessions
Source: https://docs.daimo.com/guides/sessions
Core concept: session lifecycle, statuses, and credential model
A **session** represents a single deposit attempt. It tracks the full lifecycle from creation through delivery.
## Session lifecycle
```mermaid theme={null}
stateDiagram-v2
[*] --> requires_payment_method: Create session
requires_payment_method --> waiting_payment: Set payment method
waiting_payment --> processing: Deposit detected
processing --> succeeded: Funds delivered
processing --> bounced: Delivery failed
requires_payment_method --> expired: Timeout
waiting_payment --> expired: Timeout
```
### Statuses
| Status | Description |
| ------------------------- | ------------------------------------------------------------------------------- |
| `requires_payment_method` | Session created, waiting for the user to choose how to pay |
| `waiting_payment` | Payment method set, waiting for the user's deposit transaction |
| `processing` | Deposit detected, funds are being routed to the destination |
| `succeeded` | Funds delivered to the destination address |
| `bounced` | Delivery failed (e.g. contract call reverted), funds returned to refund address |
| `expired` | Session timed out before a deposit was received |
Terminal statuses: `succeeded`, `bounced`, `expired`. Once terminal, a session cannot change status.
## Credential model
Daimo uses two types of credentials:
### API key
Your account-wide API key. Use it **server-side only** for operations that require full access:
* Creating sessions
* Retrieving full session details (including `clientSecret` and `metadata`)
Pass it as a Bearer token:
```
Authorization: Bearer
```
### Client secret
A per-session token returned when you create a session. It grants limited access to that single session:
* Setting the payment method
* Checking session status
The client secret is safe to expose in browser code. Pass it in the request body (or query string for GET requests):
```json theme={null}
{ "clientSecret": "d7a8f3b2..." }
```
## Session object
When retrieved with an API key, the full session includes:
| Field | Type | Description |
| --------------- | ---------------- | ---------------------------------------------------- |
| `sessionId` | `string` | Unique 32-character hex ID |
| `status` | `string` | One of the statuses above |
| `destination` | `object` | Where funds are delivered (see below) |
| `display` | `object` | UI metadata: `title`, `verb`, optional `themeCssUrl` |
| `paymentMethod` | `object \| null` | How the user is paying (see below) |
| `metadata` | `object \| null` | Key-value pairs set at creation |
| `clientSecret` | `string` | Per-session client credential |
| `createdAt` | `number` | Unix timestamp (seconds) |
| `expiresAt` | `number` | Unix timestamp (seconds) |
Without an API key, `metadata` and `clientSecret` are omitted (returns `SessionPublicInfo`).
### Destination
```typescript theme={null}
{
type: "evm",
address: "0x...", // destination address
chainId: 8453, // e.g. Base
chainName: "Base",
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // e.g. USDC
tokenSymbol: "USDC",
amountUnits: "10.00", // requested amount (optional)
delivery: { // set once funds are delivered
txHash: "0x...",
receivedUnits: "10.00"
}
}
```
### Payment methods
To collect a deposit, you first set a payment method on the session by calling `POST /v1/sessions/{id}/paymentMethods`. This transitions the session to `waiting_payment` and returns the information needed to complete the deposit.
Four types, depending on the source:
#### EVM
Deposit from any of our [supported EVM chains](/supported-chains#destination-chains). The response includes a single-use `receiverAddress` in `session.paymentMethod`. Display this address so the user can send funds to it.
```json theme={null}
{ "type": "evm", "receiverAddress": "0x...", "createdAt": 1700000000 }
```
#### Tron
Deposit USDT from Tron. The response includes a `tron.receiverAddress`, a single-use Tron address where the user sends USDT.
```json theme={null}
{ "type": "tron", "receiverAddress": "T...", "createdAt": 1700000000 }
```
#### Solana
Deposit from Solana. The response includes `solana.serializedTx`, a hex-encoded serialized transaction for the wallet to sign and submit.
This preserves a one-click wallet flow on Solana by bundling actions into one signed transaction.
```json theme={null}
{ "type": "solana", "createdAt": 1700000000 }
```
#### Fiat
Hosted fiat deposit. The user pays in local currency via one of the fiat rails enabled for your org (e.g. Interac in Canada, ACH and Apple Pay in the US, or SEPA in Europe), and Daimo delivers the stablecoin to your destination — same session lifecycle as any other payment method.
The response includes `fiat.hostedUrl`, a URL to a Daimo-hosted page that handles identity verification and the fiat transfer. Open it in a native [WebView](/guides/webview) or a new browser tab; the user returns to your app after completing the flow.
```json theme={null}
{ "type": "fiat", "fiatMethod": "interac", "createdAt": 1700000000 }
```
```json theme={null}
{
"fiat": {
"hostedUrl": "https://daimo.com/webview?session=...&cs=...",
"fiatMethod": "interac"
}
}
```
Pass `paymentMethod.fiatMethod` (one of `interac`, `ach`, `sepa`, `apple_pay`) to pin the hosted flow to one rail. If omitted, the hosted page lists every fiat method enabled for the org.
The `hostedUrl` is returned only once from `createPaymentMethod` — store it on the client. It is not included when retrieving the session later.
Fiat rails are enabled per-org. To request access, [contact us](mailto:support@daimo.com).
Each payment method gains a `source` object once the user's deposit transaction is detected, containing chain info, token details, and the source transaction hash.
See [Create Payment Method](/api-reference/create-payment-method) for full request/response details.
## Custom theming with `themeCssUrl`
Pass a `themeCssUrl` in `display` when creating a session to override the default Daimo UI colors, radii, and other visual tokens. The URL should point to a CSS file that redefines any of the `--daimo-*` custom properties.
Example CSS file:
```css theme={null}
:root {
--daimo-bg: #f5f0eb;
--daimo-surface: #ffffff;
--daimo-surface-secondary: #faf7f4;
--daimo-accent: #e85d04;
--daimo-text: #1a1a1a;
--daimo-text-secondary: #6b6b6b;
--daimo-border: #e0dcd7;
--daimo-radius-lg: 16px;
}
```
Available custom properties:
| Property | Description |
| --------------------------- | --------------------------------- |
| `--daimo-bg` | Page background |
| `--daimo-surface` | Card / modal background |
| `--daimo-surface-secondary` | Secondary surface for contrast |
| `--daimo-surface-hover` | Hover state background |
| `--daimo-text` | Primary body text |
| `--daimo-text-secondary` | Secondary text |
| `--daimo-text-muted` | Muted text |
| `--daimo-title` | Title / heading text |
| `--daimo-accent` | Primary accent color |
| `--daimo-success` | Success state |
| `--daimo-success-light` | Success background |
| `--daimo-checkmark` | Checkmark / confirmation color |
| `--daimo-error` | Error state |
| `--daimo-error-light` | Error background |
| `--daimo-warning` | Warning state |
| `--daimo-border` | Border color |
| `--daimo-placeholder` | Placeholder / inactive |
| `--daimo-skeleton` | Loading skeleton |
| `--daimo-qr-bg` | QR code background |
| `--daimo-qr-dot` | QR code dot color |
| `--daimo-radius-sm` | Small radius (default 0.5rem) |
| `--daimo-radius-md` | Medium radius (default 0.75rem) |
| `--daimo-radius-lg` | Large radius (default 1rem) |
| `--daimo-radius-xl` | Extra-large radius (default 20px) |
Host the CSS file on a publicly accessible URL with appropriate CORS headers. The file is loaded dynamically at runtime via a `` element.
## Polling for status
Use `PUT /v1/sessions/{id}/check` with the client secret to poll for status updates after the user has paid. This is how the modal tracks deposit progress, and how custom integrations can monitor sessions from the client side.
For most integrations, the modal handles polling automatically.
# Webhooks
Source: https://docs.daimo.com/guides/webhooks
Get notified when sessions change status
Webhooks let your server receive real-time notifications when a session transitions to a new status. Instead of polling, Daimo sends a `POST` request to your endpoint with the event payload.
## Event types
| Event | Session status | Trigger | Example use case |
| -------------------- | -------------- | ------------------------------------ | ------------------------------- |
| `session.processing` | `processing` | Deposit detected, funds being routed | Show "payment received" to user |
| `session.succeeded` | `succeeded` | Funds delivered to destination | Fulfill the order, send receipt |
| `session.bounced` | `bounced` | Delivery failed, funds refunded | Alert support, notify customer |
Subscribe to all events with `["*"]` or pick specific types.
## Quickstart
### 1. Register an endpoint
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/daimo",
"events": ["*"]
}'
```
```typescript fetch theme={null}
const response = await fetch("https://api.daimo.com/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DAIMO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/webhooks/daimo",
events: ["*"],
}),
});
const { webhook } = await response.json();
// Save webhook.secret — you'll need it to verify signatures
console.log(webhook.secret);
```
The response includes a `secret`. Store it securely, you'll use it to verify that incoming requests are from Daimo.
### 2. Handle events
Set up a route on your server to receive webhook events:
```typescript theme={null}
import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.method === "POST" && req.url === "/webhooks/daimo") {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const event = JSON.parse(body);
// TODO: verify signature (see below)
switch (event.type) {
case "session.succeeded":
// Handle successful delivery
break;
case "session.bounced":
// Handle failed delivery
break;
}
res.writeHead(200).end();
});
}
});
server.listen(4242);
```
### 3. Send a test event
Verify your endpoint is working by sending a test event:
```bash curl theme={null}
curl -X POST https://api.daimo.com/v1/webhooks/{webhookId}/test \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"eventType": "session.succeeded"}'
```
```typescript fetch theme={null}
await fetch(`https://api.daimo.com/v1/webhooks/${webhookId}/test`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DAIMO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ eventType: "session.succeeded" }),
});
```
Test events include `isTestEvent: true` in the payload so you can filter them out of your business logic.
## Verify signatures
Every webhook delivery includes a `Daimo-Signature` header for verifying authenticity. Always verify signatures in production to ensure requests are from Daimo.
### How it works
The signature header looks like this:
```
Daimo-Signature: t=1700000000,v1=5257a869...
```
To verify a webhook:
1. **Read the raw body.** Don't parse JSON first — you need the exact bytes.
2. **Extract `t` and `v1`** from the `Daimo-Signature` header by splitting on `,` and `=`.
3. **Compute HMAC-SHA256** of `${t}.${rawBody}` using your webhook secret.
4. **Compare** the computed signature to `v1` using `crypto.timingSafeEqual`.
5. **Reject stale timestamps.** If `t` is more than 5 minutes old, discard the event to prevent replay attacks.
### Full verification function
```typescript theme={null}
import * as crypto from "crypto";
const TIMESTAMP_TOLERANCE_SEC = 300; // 5 minutes
function verifyWebhookSignature(
secret: string,
signatureHeader: string,
rawBody: string,
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => {
const [k, ...v] = p.split("=");
return [k, v.join("=")];
}),
);
const ts = parts["t"];
const sig = parts["v1"];
if (!ts || !sig) return false;
const tsNum = parseInt(ts, 10);
if (isNaN(tsNum)) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - tsNum);
if (age > TIMESTAMP_TOLERANCE_SEC) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(sig, "hex"),
Buffer.from(expected, "hex"),
);
} catch {
return false;
}
}
```
### Complete handler with verification
```typescript theme={null}
import { createServer } from "node:http";
import * as crypto from "crypto";
const WEBHOOK_SECRET = process.env.DAIMO_WEBHOOK_SECRET!;
const server = createServer((req, res) => {
if (req.method === "POST" && req.url === "/webhooks/daimo") {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const signature = req.headers["daimo-signature"] as string;
if (!verifyWebhookSignature(WEBHOOK_SECRET, signature, body)) {
res.writeHead(400).end("invalid signature");
return;
}
const event = JSON.parse(body);
switch (event.type) {
case "session.succeeded":
// Handle successful delivery
break;
case "session.bounced":
// Handle failed delivery
break;
}
res.writeHead(200).end();
});
}
});
server.listen(4242);
```
## Event payload
### Field reference
| Field | Type | Description |
| -------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | Unique event ID (UUID). Use for idempotency. |
| `type` | `string` | One of `session.processing`, `session.succeeded`, `session.bounced` |
| `createdAt` | `number` | Unix timestamp (seconds) when the event was created |
| `data.session` | `object` | Session snapshot at event time. Same shape as the [session object](/guides/sessions#session-object), without `clientSecret`. |
| `isTestEvent` | `boolean` | `true` for test events sent via `/test` endpoint. Omitted for real events. |
Here's an example of a `session.succeeded` event:
```json theme={null}
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "session.succeeded",
"createdAt": 1700000000,
"data": {
"session": {
"sessionId": "abcdef1234567890abcdef1234567890",
"status": "succeeded",
"destination": {
"type": "evm",
"address": "0x...",
"chainId": 8453,
"chainName": "Base",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenSymbol": "USDC",
"amountUnits": "10.00",
"delivery": {
"txHash": "0x...",
"receivedUnits": "10.00"
}
},
"display": {
"title": "Deposit to Acme",
"verb": "Deposit"
},
"paymentMethod": {
"type": "evm",
"receiverAddress": "0x...",
"createdAt": 1700000000
},
"metadata": { "myUserId": "user_123" },
"createdAt": 1700000000,
"expiresAt": 1700003600
}
}
}
```
## Delivery behavior
Every delivery includes these headers:
| Header | Description |
| ----------------- | ------------------------------------------------------------------------------ |
| `Content-Type` | `application/json` |
| `Daimo-Signature` | `t=,v1=` (see [Verify signatures](#verify-signatures)) |
* Daimo waits **10 seconds** for your server to respond.
* Any **2xx** status code counts as success.
* Failed deliveries are retried with **exponential backoff**: the n-th retry waits 2^(n-1) minutes.
* After **10 failed attempts**, the event is marked as failed and no further retries are made.
## Test events
Use `POST /v1/webhooks/{webhookId}/test` to send a test event. You can optionally specify an `eventType` parameter (defaults to `session.succeeded`).
Test events contain `isTestEvent: true` in the payload. Use this flag to skip business logic during testing.
## Best practices
* **Return 200 quickly.** Process events asynchronously if your handler does heavy work. Daimo times out after 10 seconds.
* **Verify signatures.** Always verify the `Daimo-Signature` header in production to confirm requests are from Daimo.
* **Handle test events.** Check `event.isTestEvent` and skip side effects (e.g. order fulfillment) for test events.
* **Be idempotent.** Daimo may deliver the same event more than once. Log processed event IDs and skip duplicates. The `event.id` uniquely identifies each event.
# WebView
Source: https://docs.daimo.com/guides/webview
Embed the deposit UI in native mobile apps
Load the Daimo payment UI inside a native WebView (iOS WKWebView, Android WebView, or React Native). No SDK installation required — just create a session and construct a URL.
## How it works
1. Create a session via the API
2. Construct the webview URL from the session ID and client secret
3. Load that URL in your native WebView
4. Listen for `postMessage` events to track payment progress
## Create a session
```bash theme={null}
curl -X POST https://api.daimo.com/v1/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": {
"type": "evm",
"address": "0xYourAddress",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amountUnits": "10.00"
},
"display": {
"title": "Deposit to Acme",
"verb": "Deposit"
}
}'
```
Then construct the webview URL from the response:
```
https://daimo.com/webview?session={sessionId}&cs={clientSecret}
```
## Load the WebView
### iOS (Swift)
```swift theme={null}
import WebKit
let webView = WKWebView(frame: view.bounds)
view.addSubview(webView)
// Register message handler for payment events
let handler = PaymentMessageHandler()
webView.configuration.userContentController.add(handler, name: "daimoPay")
// Construct the URL from your session response
let urlString = "https://daimo.com/webview?session=\(sessionId)&cs=\(clientSecret)"
let url = URL(string: urlString)!
webView.load(URLRequest(url: url))
class PaymentMessageHandler: NSObject, WKScriptMessageHandler {
func userContentController(
_ controller: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard let body = message.body as? [String: Any],
let type = body["type"] as? String else { return }
switch type {
case "ready": print("Payment UI loaded")
case "paymentStarted": print("Payment initiated")
case "paymentCompleted": print("Payment succeeded")
default: break
}
}
}
```
### Android (Kotlin)
```kotlin theme={null}
val webView = WebView(this)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.addJavascriptInterface(object {
@JavascriptInterface
fun postMessage(message: String) {
val json = JSONObject(message)
when (json.getString("type")) {
"ready" -> Log.d("Daimo", "Payment UI loaded")
"paymentStarted" -> Log.d("Daimo", "Payment initiated")
"paymentCompleted" -> Log.d("Daimo", "Payment succeeded")
}
}
}, "ReactNativeWebView")
// Construct the URL from your session response
val url = "https://daimo.com/webview?session=$sessionId&cs=$clientSecret"
webView.loadUrl(url)
```
### React Native
```tsx theme={null}
import { WebView } from "react-native-webview";
function PaymentWebView({
sessionId,
clientSecret,
}: {
sessionId: string;
clientSecret: string;
}) {
const uri = `https://daimo.com/webview?session=${sessionId}&cs=${clientSecret}`;
const handleMessage = (event: { nativeEvent: { data: string } }) => {
const msg = JSON.parse(event.nativeEvent.data);
switch (msg.type) {
case "ready":
console.log("Payment UI loaded");
break;
case "paymentStarted":
console.log("Payment initiated");
break;
case "paymentCompleted":
console.log("Payment succeeded");
break;
}
};
return (
);
}
```
## Query parameters
| Parameter | Required | Values | Description |
| --------- | -------- | ---------------- | ---------------------------------------- |
| `session` | Yes | string | Session ID |
| `cs` | Yes | string | Client secret |
| `locale` | No | `es`, `fr`, etc. | UI language (default: browser locale) |
| `theme` | No | `light`, `dark` | Color theme (default: auto) |
| `layout` | No | `embed` | Renders inline; omit for modal (default) |
## Handle events
The WebView sends messages via `postMessage`. The message format:
```json theme={null}
{
"source": "daimo-pay",
"version": 1,
"type": "paymentCompleted",
"payload": {}
}
```
| Event | Description |
| ------------------ | -------------------------------------- |
| `ready` | Payment UI finished loading |
| `modalOpened` | Modal became visible |
| `modalClosed` | Modal was dismissed |
| `paymentStarted` | User's deposit transaction is detected |
| `paymentCompleted` | Funds delivered to destination |
Events are notifications only — the `payload` object is empty. To get full session details (tx hash, chain, amounts, etc.) after any event, poll [`GET /v1/sessions/{sessionId}`](/api-reference/retrieve-session).
## Theming
To customize the payment UI, pass a `themeCssUrl` in `display` when creating the session. This lets you override colors, radii, and other visual tokens. See [Custom theming with `themeCssUrl`](/guides/sessions#custom-theming-with-themecssurl) for the full list of CSS custom properties.
## sendToHost protocol
The webview page communicates with the host container via a `sendToHost` function that tries three transport layers in order:
1. **React Native / Expo** — `window.ReactNativeWebView.postMessage(JSON.stringify(msg))`
2. **iOS / macOS WKWebView** — `window.webkit.messageHandlers.daimoPay.postMessage(msg)`
3. **Browser iframe / desktop WebView** — `window.parent.postMessage(msg, "*")`
All messages share a common envelope:
```json theme={null}
{
"source": "daimo-pay",
"version": 1,
"type": "",
"payload": {}
}
```
# Introduction
Source: https://docs.daimo.com/introduction
The ramp for stablecoin apps
Daimo is the ramp for stablecoin apps. Integrate once, accept deposits from any wallet, any chain, any token. Funds arrive as the stablecoin you want, on the chain you want.
Global fiat onramp support is coming soon. [Book a demo](https://daimo.com) to learn more.
## How it works
1. **Create a session**: specify destination chain, token, and amount
2. **Collect the deposit**: user pays via the Daimo modal or your custom UI
3. **Funds settle**: stablecoins arrive at your destination address on-chain
## Integration paths
### Modal SDK
Pre-built React deposit UI via `@daimo/sdk`. Fastest way to integrate.
* Handles chain selection, wallet connection, and transaction signing
* Supports embedded and overlay modes
* \~30 lines of code to get started
### API
A plain REST API.
* Full control over the deposit experience
* Build your own custom UI
* Use from any backend: Python, Go, Ruby, etc.
Both paths use the same session-based API. The modal SDK is a thin React wrapper around the API.
## Next steps
Get a working deposit flow in 5 minutes
Understand the session lifecycle
Browse all endpoints
See destination and source chains
# Quickstart
Source: https://docs.daimo.com/quickstart
Accept your first deposit in 5 minutes
This guide gets a working deposit flow running with the Daimo modal SDK. You'll create a session on your server and render a pre-built deposit UI on the client.
## Install
```bash theme={null}
npm install @daimo/sdk
```
## Step 1: Create a session (server-side)
Create a session with your API key on the server - never expose it to the client. The response includes a `clientSecret` you provide to your frontend.
For details on credential handling, see Daimo's [Credential Model](/guides/sessions#credential-model).
```typescript theme={null}
// In your API route handler:
const response = await fetch("https://api.daimo.com/v1/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DAIMO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: {
type: "evm",
address: "0xYourAddress",
chainId: 8453, // Base
tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
amountUnits: "10.00", // $10 USDC
},
display: {
title: "Deposit to Acme",
verb: "Deposit",
},
}),
});
const { session } = await response.json();
// Return session.clientSecret and session.sessionId to the frontend
```
## Step 2: Render the modal (client-side)
Pass the `clientSecret` to your React frontend and render ``.
```tsx theme={null}
import { DaimoSDKProvider, DaimoModal } from "@daimo/sdk/web";
import "@daimo/sdk/web/theme.css";
function App() {
return (
);
}
function DepositPage() {
// Fetch sessionId and clientSecret from your backend
const [sessionId, setSessionId] = useState(null);
const [clientSecret, setClientSecret] = useState(null);
useEffect(() => {
fetch("/api/create-session", { method: "POST" })
.then((res) => res.json())
.then((data) => {
setSessionId(data.session.sessionId);
setClientSecret(data.session.clientSecret);
});
}, []);
if (!sessionId || !clientSecret) return null;
return (
console.log("deposit succeeded")}
/>
);
}
```
## Step 3: Handle completion
When the deposit completes, poll for status using `GET /v1/sessions/{id}`. See [Check Session](/api-reference/check-session).
The session's `destination.delivery.txHash` contains the on-chain transaction which delivers the funds to the destination address.
## Next steps
* [Sessions](/guides/sessions) - understand the full session lifecycle
* [Modal](/guides/modal) - customize the modal UI
* [Custom Integration](/guides/custom-integration) - build your own UI with the API
# Supported Chains
Source: https://docs.daimo.com/supported-chains
Destination and source chains for deposits
## Destination chains
Deposits can be delivered to any of these EVM chains:
| Chain | Chain ID | USDC Address |
| --------------- | -------- | -------------------------------------------- |
| Arbitrum | `42161` | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` |
| Base | `8453` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| BNB Smart Chain | `56` | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` |
| Celo | `42220` | `0xcebA9300f2b948710d2653dD7B07f33A8B32118C` |
| Ethereum | `1` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` |
| Gnosis | `100` | `0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0` |
| HyperEVM | `999` | `0xb88339CB7199b77E23DB6E890353E22632Ba630f` |
| Linea | `59144` | `0x176211869cA2b568f2A7D4EE941E073a821EE1ff` |
| Monad | `143` | `0x754704Bc059F8C67012fEd69BC8A327a5aafb603` |
| Optimism | `10` | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` |
| Polygon | `137` | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` |
| Worldchain | `480` | `0x79A02482A880bCE3F13e09Da970dC34db4CD24d1` |
Additional destination tokens (USDT, DAI, ETH, etc.) are available on most chains. See the full token list in the [SDK source](https://github.com/daimo-eth/pay/blob/master/packages/sdk/src/common/token.ts).
## Source chains
Users can deposit from:
* **All EVM chains above**: any supported token (stablecoins and any other liquid ERC-20 tokens).
* **Solana**: USDC, USDT, SOL, and any other liquid tokens.
* **Tron**: USDT
Source chain and token availability is automatic. When a user connects their wallet, Daimo detects their balances and shows available deposit options.