> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ripio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ripio on/off-ramp for Privy wallets with 1:1 local stablecoins

> Add Ripio's on/off-ramp for wARS, wBRL, wMXN and wCOP to Privy's self-custodial embedded wallets: PIX, SPEI, PSE and bank transfers.

This recipe shows how to add a fiat on-ramp and off-ramp to [Privy](https://privy.io) embedded wallets with Ripio's [Ramps API](https://docs.ripio.com/ramps-api/overview/introduction.md), so users in [Argentina, Brazil, Mexico and Colombia](https://docs.ripio.com/ramps-api/overview/countries/overview.md) can turn local currency into [wARS, wBRL, wMXN or wCOP](https://docs.ripio.com/wfiat/overview.md), each pegged 1:1 to its currency, and back, in their own self-custodial wallet, without leaving your app. Because everything runs through the API, you design the ramp experience in your own UI: amount entry, [quotes](https://docs.ripio.com/ramps-api/quotes/create-quote.md), payment instructions and [status updates](https://docs.ripio.com/ramps-api/webhooks/introduction.md). [Identity verification](https://docs.ripio.com/ramps-api/kyc/overview.md) adds a Ripio-hosted step for document upload and liveness.

It's most useful for wallets, fintechs, and remittance and payments apps serving users in Latin America who want to hold, spend or settle in their own currency on-chain. Users pay with the rails they already use: bank transfer or Mercado Pago in Argentina, PIX in Brazil, SPEI in Mexico, and PSE or Bre-B in Colombia.

> [!NOTE]
>
> Privy wallets are self-custodial: Ripio doesn't hold or control them, and Privy is a third-party provider. Ripio handles the customer's KYC, the fiat side and the conversion.

With Privy and Ripio, your app can:

* Provision an embedded wallet for each user.
* Build a fully custom ramp flow with quotes, orders, payment instructions and webhooks, all inside your app.
* Receive wFiat directly in the user's Privy wallet.
* Track every order in real time with webhooks.
* Let users off-ramp wFiat back to local currency, signing the transfer with their Privy wallet.

## Supported countries and payment methods

The Ramps API operates per country, and **each Ripio account is tied to a single country and its local currency**. To serve several countries, you need one Ripio account (and one set of credentials) per country.

| Country | Stablecoin | Users pay with (on-ramp) | Users get paid to (off-ramp) |
| - | - | - | - |
| ![](https://flagcdn.com/ar.svg)[Argentina](https://docs.ripio.com/ramps-api/overview/countries/argentina.md) | ![wARS](https://b2b-widget-onramp.ripio.com/wars.png)**wARS** | `bank_transfer` (CVU), `mercadopago` | `bank_transfer` (CBU / CVU / alias) |
| ![](https://flagcdn.com/br.svg)[Brazil](https://docs.ripio.com/ramps-api/overview/countries/brazil.md) | ![wBRL](https://b2b-widget-onramp.ripio.com/wbrl.png)**wBRL** | `pix` (BR Code) | `pix` (PIX key) |
| ![](https://flagcdn.com/mx.svg)[Mexico](https://docs.ripio.com/ramps-api/overview/countries/mexico.md) | ![wMXN](https://b2b-widget-onramp.ripio.com/wmxn.png)**wMXN** | `bank_transfer` (SPEI to a CLABE) | `bank_transfer` (CLABE) |
| ![](https://flagcdn.com/co.svg)[Colombia](https://docs.ripio.com/ramps-api/overview/countries/colombia.md) | ![wCOP](https://b2b-widget-onramp.ripio.com/wcop.png)**wCOP** | `bank_transfer` (PSE), `breb`, `r2p_bancolombia`, `r2p_nequi` | `bank_transfer` (PSE), `breb` |

The values in code font are the `paymentMethodType` you send to the API. Each country also has a few rules to plan for:

* ![](https://flagcdn.com/ar.svg)**Argentina:** the CVU to pay into comes back with each order. Payout accounts must be confirmed before use.
* ![](https://flagcdn.com/br.svg)**Brazil:** every order carries its own BR Code, valid until `expiresAt` and payable only from the customer's own CPF/CNPJ. Customers must complete the [investor profile](https://docs.ripio.com/ramps-api/investor-profile/introduction.md). No deposit account is needed, and the PIX key's holder must match the customer's CPF/CNPJ (phone keys need `+55`).
* ![](https://flagcdn.com/mx.svg)**Mexico:** on-ramp needs an enabled deposit account first. For off-ramp, the bank account owner's name and RFC must match the customer's KYC. From November 30, 2026, every order and session needs a `travelRule` declaring who owns the wallet (`20108` otherwise): see [Travel Rule (Mexico)](https://docs.ripio.com/ramps-api/travel-rule/introduction.md).
* ![](https://flagcdn.com/co.svg)**Colombia:** PSE and R2P orders need `extraData.callback`, and PSE also needs `extraData.bankCode` (from List Banks). `breb` needs an enabled deposit account and confirmed payout accounts.

The minimum operation is the local-currency equivalent of USD 1 in every country. Read the current per-transaction minimums and maximums with [Get Transaction Limits](https://docs.ripio.com/ramps-api/transaction-limits/get-transaction-limits.md) instead of hardcoding them.

> [!NOTE]
>
> wPEN and wCLP aren't supported by the Ramps API yet. Users can already hold, transfer and bridge them in their Privy wallet. See [Countries and payment methods](https://docs.ripio.com/ramps-api/overview/countries/overview.md).

## Prerequisites

* A Privy account with an app configured ([dashboard.privy.io](https://dashboard.privy.io)).
* A React app using `@privy-io/react-auth`, plus a backend to call the Ramps API.
* Ramps API credentials (`client_id` and `client_secret`, sandbox or production). [Register your company](https://platform.ripio.com/register) to get them.

> [!WARNING]
>
> Ramps API credentials must stay on your server. Never call the Ramps API from the browser.

## Contract addresses

wFiat tokens are ERC-20s with 18 decimals, and each one uses the same address on every supported EVM chain. See [Contracts](https://docs.ripio.com/wfiat/contracts.md) for the networks and the Tempo addresses.

| Token | Address (EVM chains) |
| - | - |
| ![wARS](https://b2b-widget-onramp.ripio.com/wars.png)**wARS** ![](https://flagcdn.com/ar.svg) | `0x0DC4F92879B7670e5f4e4e6e3c801D229129D90D` |
| ![wBRL](https://b2b-widget-onramp.ripio.com/wbrl.png)**wBRL** ![](https://flagcdn.com/br.svg) | `0xD76f5Faf6888e24D9F04Bf92a0c8B921FE4390e0` |
| ![wMXN](https://b2b-widget-onramp.ripio.com/wmxn.png)**wMXN** ![](https://flagcdn.com/mx.svg) | `0x337E7456B420bD3481e7FA61fA9850343d610d34` |
| ![wCOP](https://b2b-widget-onramp.ripio.com/wcop.png)**wCOP** ![](https://flagcdn.com/co.svg) | `0x8a1D45e102e886510e891d2Ec656a708991e2D76` |

This recipe uses **wARS on Celo**. To ramp wBRL with PIX, wMXN with SPEI or wCOP with PSE, change the currency codes and `paymentMethodType`: the flow is the same. Check which chains are rampable for each token with the networks endpoints in step 2.

## Recipe

### 1. Create a Privy embedded wallet

Install the Privy React SDK:

```bash theme={null}
npm install @privy-io/react-auth
```

Wrap your app in `PrivyProvider` and have Privy create an EVM embedded wallet when the user logs in:

```tsx providers.tsx theme={null}
'use client';

import { PrivyProvider } from '@privy-io/react-auth';
import { celo } from 'viem/chains';

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PrivyProvider
      appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
      config={{
        loginMethods: ['email', 'sms', 'google'],
        embeddedWallets: {
          ethereum: { createOnLogin: 'users-without-wallets' },
        },
        defaultChain: celo,
        supportedChains: [celo],
      }}
    >
      {children}
    </PrivyProvider>
  );
}
```

Read the user's embedded wallet address:

```tsx use-embedded-address.ts theme={null}
import { usePrivy, useWallets } from '@privy-io/react-auth';

export function useEmbeddedAddress() {
  const { ready, authenticated } = usePrivy();
  const { wallets } = useWallets();
  const embedded = wallets.find((w) => w.walletClientType === 'privy');
  return ready && authenticated ? embedded?.address : undefined;
}
```

### 2. Set up the Ramps API on your backend

The Ramps API is served from `https://skala.ripio.com` (sandbox: `https://skala-sandbox.ripio.com`).

**Authenticate.** Exchange your credentials for an access token with the OAuth2 client credentials flow (`POST /oauth2/token/`, Basic auth with `client_id:client_secret`, `grant_type=client_credentials`), then send it as `Authorization: Bearer <access_token>` on every call. See [Acquire Access Token](https://docs.ripio.com/ramps-api/authentication/acquire-access-token.md). Tokens are long-lived (`expires_in`), so cache them.

```ts ripio.ts theme={null}
// Server-side only
const RIPIO = process.env.RIPIO_BASE_URL ?? 'https://skala-sandbox.ripio.com';

let cachedToken: { value: string; expiresAt: number } | null = null;

async function getRipioToken() {
  if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value;

  const basic = Buffer.from(`${process.env.RIPIO_CLIENT_ID}:${process.env.RIPIO_CLIENT_SECRET}`).toString('base64');
  const res = await fetch(`${RIPIO}/oauth2/token/`, {
    method: 'POST',
    headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ grant_type: 'client_credentials' }),
  });
  if (!res.ok) throw new Error(`Ripio auth failed: ${res.status}`);

  const { access_token, expires_in } = await res.json();
  cachedToken = { value: access_token, expiresAt: Date.now() + (expires_in - 60) * 1000 }; // refresh a minute early
  return access_token as string;
}

export async function ripio<T = any>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> {
  const res = await fetch(`${RIPIO}${path}`, {
    method,
    headers: { Authorization: `Bearer ${await getRipioToken()}`, 'Content-Type': 'application/json' },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) throw await res.json().catch(() => ({ status: res.status })); // { code, type, detail, status }: branch on `code`
  return res.json();
}
```

**Look up the network and currency codes once.** The Ramps API identifies networks and assets by name (for example `chain` and `toCurrency` in a quote). Find the names Ripio uses for wARS on Celo by matching the token's contract address, and store them as configuration (for example `RIPIO_CHAIN` and `RIPIO_WARS_CODE`). Use [Get Withdrawal Networks](https://docs.ripio.com/ramps-api/networks/get-withdrawal-networks.md) for on-ramp (Ripio sends the token to the user) and [Get Deposit Networks](https://docs.ripio.com/ramps-api/networks/get-deposit-networks.md) for off-ramp (the user sends it to Ripio).

```ts find-networks.ts theme={null}
// Server-side, run once at setup
import { ripio } from './ripio';

type Network = { network_name: string; assets?: { name: string; contract_address: string | null }[] };

export async function findWfiatNetworks(kind: 'withdrawal' | 'deposit', token: string) {
  const networks = await ripio<Network[]>('GET', `/api/v1/${kind}Networks/?include_currency=true`);
  return networks.flatMap((n) =>
    (n.assets ?? [])
      .filter((a) => a.contract_address?.toLowerCase() === token.toLowerCase())
      .map((a) => ({ chain: n.network_name, currency: a.name })),
  );
}

// In a one-off setup script. wFiat uses the same address on every EVM chain, so each call
// returns one entry per network: pick the one for your Privy wallet's chain (Celo here) and
// check it shows up in both lists, since on-ramp and off-ramp use the same RIPIO_CHAIN.
// await findWfiatNetworks('withdrawal', '0x0dc4f92879b7670e5f4e4e6e3c801d229129d90d');
// await findWfiatNetworks('deposit', '0x0dc4f92879b7670e5f4e4e6e3c801d229129d90d');
```

**Onboard each user once.** Map each Privy user to a Ripio customer:

**1. Create the customer**

[Create Customer](https://docs.ripio.com/ramps-api/customers/create-customer.md): `POST /api/v1/customers/` with `email` and `type: 'INDIVIDUAL'`. Store the returned `customerId` next to the Privy user ID.

**2. Accept the terms and conditions**

Fetch the active [terms](https://docs.ripio.com/ramps-api/terms-and-conditions/introduction.md) and record the acceptance with `POST /api/v1/customers/{customerId}/acceptTerms/`. Terms are versioned per country, and a new version blocks the customer until they accept it (error `20039`), so check the acceptance status before operating instead of storing it.

**3. Verify identity (KYC)**

Run [KYC](https://docs.ripio.com/ramps-api/kyc/overview.md) through a Ripio-hosted redirect or submit it through the API. Document upload and liveness happen in a Ripio-hosted step, and logging in with Privy doesn't verify identity. If the customer is already verified in your own Sumsub account, send a share token in `kycProviderShareToken` and, when both verifications are compatible, they skip that step and stay in your app. It's opt-in: see [Reusable KYC (Sumsub share token)](https://docs.ripio.com/ramps-api/kyc/reusable-kyc.md). The customer can operate once KYC is `COMPLETED`, and the `KYC.*` webhooks tell you when that happens.

**4. Brazil only: investor profile**

Collect the [investor profile](https://docs.ripio.com/ramps-api/investor-profile/introduction.md). Without it, orders, sessions and payout accounts fail with `20100`.

**5. Create the deposit account, where required**

Where the country requires it (see [Supported countries and payment methods](#supported-countries-and-payment-methods)), create the customer's [deposit account](https://docs.ripio.com/ramps-api/customers/create-deposit-account.md) and wait for the `ACCOUNT.ENABLED` webhook.

### 3. On-ramp: local currency to wFiat in the Privy wallet

**Get a quote.** `POST /api/v1/quotes/` holds a price for a short time and returns a `quoteId`. See [Create Quote](https://docs.ripio.com/ramps-api/quotes/create-quote.md).

**Create the order.** `POST /api/v1/onramp/` with the `quoteId`, the `customerId`, your own `externalRef` (a UUID) and the user's **Privy wallet address** as `depositAddress`. See [Create On-Ramp Order](https://docs.ripio.com/ramps-api/on-ramp/create-on-ramp-order.md). The response includes `fiatPaymentInstructions`: for example a CVU in Argentina, a CLABE in Mexico or a PIX BR Code in Brazil.

```ts on-ramp.ts theme={null}
// Server-side, called by your app for the logged-in Privy user
import { ripio } from './ripio';

export async function createOnRamp(
  customerId: string,
  privyAddress: string,
  amountArs: string,
  externalRef: string, // a UUID you generate once per purchase and store with the order
) {
  const quote = await ripio('POST', '/api/v1/quotes/', {
    customerId,
    fromCurrency: 'ARS',
    toCurrency: process.env.RIPIO_WARS_CODE!,
    fromAmount: amountArs,
    chain: process.env.RIPIO_CHAIN!,
    paymentMethodType: 'bank_transfer',
  });

  const order = await ripio('POST', '/api/v1/onramp/', {
    customerId,
    quoteId: quote.quoteId,
    depositAddress: privyAddress, // the user's Privy embedded wallet
    externalRef,
    travelRule: { counterpartyIsSelf: true }, // Mexico: required from 2026-11-30; the user owns their Privy wallet
  });

  return { quote, transaction: order.transaction, paymentInstructions: order.fiatPaymentInstructions };
}
```

**Show the payment instructions in your own UI.** Render the quote and `fiatPaymentInstructions` however fits your product: a copyable CVU, a PIX QR code, a status timeline. The user pays from their bank. Once the deposit is detected and the trade executes, Ripio sends the wFiat to the Privy wallet and the order moves to `COMPLETED`.

> [!TIP]
>
> If the country requires a deposit account and it isn't ready, the order call returns `400`: code `40019` means it was never created (create it, don't retry), and `40014` means it's still `processing` (wait for `ACCOUNT.ENABLED`, then retry). See the [On-Ramp integration guide](https://docs.ripio.com/ramps-api/guides/on-ramp-integration.md).
>
> For recurring top-ups, an [On-Ramp Session](https://docs.ripio.com/ramps-api/on-ramp/create-on-ramp-session.md) gives the user a reusable deposit account tied to their Privy address: every transfer to it becomes an order at the market rate. Sessions work only for `bank_transfer` in Argentina and Mexico and `breb` in Colombia: in Brazil, every order carries its own BR Code.

### 4. Track orders with webhooks

The Ramps API notifies your backend of every status change, so you don't need to poll. Ask the Ripio technical team to register your HTTPS endpoint. KYC events go to a separate `kyc_webhook_url` that Ripio also configures.

Every event is a JSON `POST` with `eventType` and `issueDatetime`. The body key depends on the event family: `transactionObject` for on-ramp and off-ramp events, `payload` for KYC events and `account` for account events.

| Family | Events |
| - | - |
| [On-ramp](https://docs.ripio.com/ramps-api/webhooks/on-ramp-events.md) | `ON-RAMP.DEPOSIT.RECEIVED`, `ON-RAMP.TRADE.COMPLETED`, `ON-RAMP.WITHDRAWAL.PROCESSING`, `ON-RAMP.WITHDRAWAL.COMPLETED` (includes `txnHash`), `ON-RAMP.ORDER.CANCELLED`, `ON-RAMP.ORDER.REFUNDED`, plus `ON-RAMP.DEPOSIT.REJECTED` and `ON-RAMP.DEPOSIT.REFUNDED` for fiat deposits that matched no order. Mexico: `ON-RAMP.TRAVEL_RULE.REQUIRED` |
| [Off-ramp](https://docs.ripio.com/ramps-api/webhooks/off-ramp-events.md) | `OFF-RAMP.DEPOSIT.RECEIVED`, `OFF-RAMP.TRADE.COMPLETED`, `OFF-RAMP.WITHDRAWAL.PROCESSING`, `OFF-RAMP.WITHDRAWAL.COMPLETED`, `OFF-RAMP.ORDER.CANCELLED`, `OFF-RAMP.ORDER.REFUNDED` (a crypto refund completed on-chain), plus `OFF-RAMP.DEPOSIT.REJECTED` and `OFF-RAMP.DEPOSIT.REFUNDED` for crypto deposits that matched no order. Mexico: `OFF-RAMP.TRAVEL_RULE.REQUIRED` |
| [KYC](https://docs.ripio.com/ramps-api/webhooks/kyc-events.md) | `KYC.INCOMPLETE_USER_DATA`, `KYC.IN_REVIEW`, `KYC.COMPLETED`, `KYC.FAILED` |
| [Account](https://docs.ripio.com/ramps-api/webhooks/account-events.md) | `ACCOUNT.PROCESSING`, `ACCOUNT.ENABLED`, `ACCOUNT.DISABLED` (deposit account readiness), `ACCOUNT.CLOSED` and `ACCOUNT.REOPENED` |

On both ramps, only a completed refund emits an event: a refund that is registered, approved or rejected is visible only in `latestRefund`. [Sell and Pay](https://docs.ripio.com/ramps-api/webhooks/sell-and-pay-events.md) and [support ticket](https://docs.ripio.com/ramps-api/webhooks/support-ticket-events.md) events are also available.

**Verify every request.** Ripio signs the raw body with HMAC-SHA256 using a shared secret and sends it in the `Http-X-Wh-Signature-256` header as `sha256=<hex>`. See [signature validation](https://docs.ripio.com/ramps-api/webhooks/introduction.md). Compute the HMAC over the raw bytes, never over re-serialized JSON.

```ts verify-webhook.ts theme={null}
import { createHmac, timingSafeEqual } from 'crypto';

// Node lowercases header names: pass req.headers['http-x-wh-signature-256']
export function verifyRipioSignature(rawBody: Buffer, secret: string, header: string | undefined) {
  if (!header) return false;
  const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'));
  const received = Buffer.from(header);
  return expected.length === received.length && timingSafeEqual(expected, received);
}
```

Respond with a `2xx` within 10 seconds and do the heavy work asynchronously. Events can be delivered more than once and out of order, so make your handler idempotent. If a firewall filters by user agent, allow-list `Ripio-Webhooks/1.0`.

A common pattern: on `ON-RAMP.WITHDRAWAL.COMPLETED`, refresh the user's wFiat balance in the Privy wallet and notify them.

### 5. Read the wFiat balance in the Privy wallet

```ts balance.ts theme={null}
import { createPublicClient, http, erc20Abi, formatUnits } from 'viem';
import { celo } from 'viem/chains';

export const WARS = '0x0dc4f92879b7670e5f4e4e6e3c801d229129d90d'; // same address on every supported EVM chain

const client = createPublicClient({ chain: celo, transport: http() });

export async function getWarsBalance(owner: `0x${string}`) {
  const raw = await client.readContract({
    address: WARS,
    abi: erc20Abi,
    functionName: 'balanceOf',
    args: [owner],
  });
  return formatUnits(raw, 18);
}
```

Refresh it when your backend receives `ON-RAMP.WITHDRAWAL.COMPLETED`, or listen for `Transfer` events to the user's address.

### 6. Off-ramp: wFiat to a local bank account or PIX key

The recommended off-ramp is an [Off-Ramp Session](https://docs.ripio.com/ramps-api/guides/off-ramp-integration.md): you set it up once per user against their payout account, and every wFiat transfer the user sends to the session's deposit address becomes an off-ramp order automatically, at a fresh quote. There's no quote to race against while the user signs.

**Register and confirm the payout account.** Fetch the fields for the country with `GET /api/v1/fiatAccounts/requirements/`, then call [Create Fiat Account](https://docs.ripio.com/ramps-api/fiat-accounts/create-fiat-account.md). In Argentina, Brazil, Mexico and Colombia (`breb`), confirm the payout account with [Confirm Fiat Account](https://docs.ripio.com/ramps-api/fiat-accounts/confirm-fiat-account.md) once it reaches `UNCONFIRMED`:

* **Argentina** (`bank_transfer`) and **Brazil** (`pix`) validate the account during the request, so it comes back already `UNCONFIRMED`, with the resolved holder in `metadata`.
* **Mexico** (`bank_transfer`) and **Colombia** (`breb`) validate it asynchronously: it starts as `PROCESSING` and becomes `UNCONFIRMED` later. Poll [List Fiat Accounts](https://docs.ripio.com/ramps-api/fiat-accounts/list-fiat-accounts.md) for the transition.

Show the holder in `metadata` to the user before confirming. The holder must be the customer: an account in someone else's name (for example a PIX key whose holder document doesn't match the customer's CPF) is created as `DISABLED`. After confirmation the account moves to `ENABLED` and can receive payouts.

```ts payout-account.ts theme={null}
// Server-side
import { ripio } from './ripio';

// 1. Create the account and return the resolved holder to your UI
export async function createPayoutAccount(customerId: string, cvuOrAlias: string) {
  const account = await ripio('POST', '/api/v1/fiatAccounts/', {
    customerId,
    paymentMethodType: 'bank_transfer',
    accountFields: { alias_or_cvu_destination: cvuOrAlias }, // Argentina; other countries use other fields
  });

  if (account.status === 'DISABLED') throw new Error('Payout account does not belong to the customer');

  // In Mexico and Colombia (breb) it starts as PROCESSING: poll List Fiat Accounts until it's UNCONFIRMED
  return { fiatAccountId: account.fiatAccountId as string, status: account.status, metadata: account.metadata };
}

// 2. Call it once the user has reviewed the holder in metadata and approved it
export async function confirmPayoutAccount(fiatAccountId: string) {
  await ripio('POST', `/api/v1/fiatAccounts/${fiatAccountId}/confirm/`);
  // The account then moves to ENABLED: wait for it before creating the session
}
```

**Create the off-ramp session** once the account is `ENABLED`. See [Create Off-Ramp Session](https://docs.ripio.com/ramps-api/off-ramp/create-off-ramp-session.md). The response lists one deposit address per supported network: keep the one for your chain. A customer can have only one active session, so store `sessionId` and the address and reuse them.

```ts off-ramp-session.ts theme={null}
// Server-side, once per user
import { ripio } from './ripio';

export async function createOffRampSession(fiatAccountId: string) {
  const session = await ripio('POST', '/api/v1/offrampSession/', {
    fiatAccountId,
    travelRule: { counterpartyIsSelf: true }, // Mexico: required from 2026-11-30; the user sends from their own wallet
  });
  const deposit = session.depositAddresses.find((d: { chain: string }) => d.chain === process.env.RIPIO_CHAIN);
  if (!deposit) throw new Error('No deposit address on the configured chain');
  return { sessionId: session.sessionId as string, depositAddress: deposit.address as `0x${string}` };
}
```

**Send wFiat from the Privy wallet** to the session's deposit address, on the same chain:

```tsx use-send-wars.ts theme={null}
import { useSendTransaction } from '@privy-io/react-auth';
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem';
import { WARS } from './balance';

export function useSendWars() {
  const { sendTransaction } = useSendTransaction();

  return (depositAddress: `0x${string}`, amountWars: string) =>
    sendTransaction({
      to: WARS,
      chainId: 42220, // Celo: must match the chain of the deposit address
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'transfer',
        args: [depositAddress, parseUnits(amountWars, 18)],
      }),
    });
}
```

Follow each order with the `OFF-RAMP.*` webhooks until the payout lands in the user's account.

**If an order is cancelled,** read `refundable` and `failureReason` on the order to resolve it:

* `refundable: true`: the trade never executed, so the wFiat is still there. Return it with a [crypto refund](https://docs.ripio.com/ramps-api/refunds/overview.md). `OFF-RAMP.ORDER.REFUNDED` arrives once it's back on-chain (read `refundedTxnHash` on the order for the hash).
* `refundable: false`: the wFiat was already sold and the fiat payout failed, usually with `failureReason: INVALID_DESTINATION`. Point the order at another payout account with [Change Fiat Account and Reprocess](https://docs.ripio.com/ramps-api/off-ramp/change-fiat-account.md). There is nothing to refund.

> [!NOTE]
>
> **Need a locked rate for a specific amount?** Use [Create Off-Ramp Order](https://docs.ripio.com/ramps-api/off-ramp/create-off-ramp-order.md) instead: get an off-ramp quote (`fromCurrency` is the wFiat code, `fromAmount` in wFiat), then `POST /api/v1/offramp/` with `customerId`, `quoteId`, `fiatAccountId` and `externalRef` (plus `travelRule` on every order in Mexico). The response returns a `depositAddress` with its `chain`.
>
> Quotes are held only briefly (for example 30 seconds), so have the user sign the transfer right away: otherwise the order can be cancelled with `failureReason: EXPIRED_QUOTE`. The deposit also has to match the order exactly (address, amount and network). One that doesn't arrives as `OFF-RAMP.DEPOSIT.REJECTED` and has to be refunded. Session deposits never hit this case.

## Test in sandbox

Point `RIPIO_BASE_URL` at `https://skala-sandbox.ripio.com` with your sandbox credentials. See [KYC in sandbox](https://docs.ripio.com/ramps-api/sandbox/kyc-in-sandbox.md) to get customers to `COMPLETED`, and simulate the user's fiat payment for an on-ramp with [Simulate Deposit](https://docs.ripio.com/ramps-api/sandbox/simulate-deposit-for-on-ramp-sandbox.md), using the value from `fiatPaymentInstructions`.

## Moving wFiat across chains

The [Ripio Bridge](https://docs.ripio.com/wfiat/bridge.md) moves wFiat between Ethereum, Base, World Chain, BNB Smart Chain, Polygon, Gnosis, Celo and Arc (wARS and wBRL also on HyperEVM) with a single contract call. Tempo isn't covered.

## Transparency

Ripio publishes collateral attestations for wARS, wBRL, wMXN and wCOP, certified by local public accountants, on the [local stablecoins page](https://www.ripio.com/en/cryptos/local-stablecoins).

## Resources

* [Ramps API introduction](https://docs.ripio.com/ramps-api/overview/introduction.md)
* [On-Ramp integration guide](https://docs.ripio.com/ramps-api/guides/on-ramp-integration.md) and [Off-Ramp integration guide](https://docs.ripio.com/ramps-api/guides/off-ramp-integration.md)
* [wFiat contracts and networks](https://docs.ripio.com/wfiat/contracts.md)
* [Ripio Bridge developer guide](https://docs.ripio.com/wfiat/bridge.md)
* [Privy React SDK setup](https://docs.privy.io/basics/react/setup)

Questions about wFiat or the Ramps API? Write to [b2b@ripio.com](mailto:b2b@ripio.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.