Skip to main content
This recipe shows how to add a fiat on-ramp and off-ramp to Privy embedded wallets with Ripio’s Ramps API, so users in Argentina, Brazil, Mexico and Colombia can turn local currency into wARS, wBRL, wMXN or wCOP, 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, payment instructions and status updates. Identity verification 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.
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. The values in code font are the paymentMethodType you send to the API. Each country also has a few rules to plan for:
  • Argentina: the CVU to pay into comes back with each order. Payout accounts must be confirmed before use.
  • 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. No deposit account is needed, and the PIX key’s holder must match the customer’s CPF/CNPJ (phone keys need +55).
  • 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).
  • 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 instead of hardcoding them.
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.

Prerequisites

  • A Privy account with an app configured (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 to get them.
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 for the networks and the Tempo addresses. 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:
Wrap your app in PrivyProvider and have Privy create an EVM embedded wallet when the user logs in:
providers.tsx
Read the user’s embedded wallet address:
use-embedded-address.ts

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. Tokens are long-lived (expires_in), so cache them.
ripio.ts
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 for on-ramp (Ripio sends the token to the user) and Get Deposit Networks for off-ramp (the user sends it to Ripio).
find-networks.ts
Onboard each user once. Map each Privy user to a Ripio customer:
1

Create the customer

Create Customer: 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 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 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). 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. 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), create the customer’s deposit account 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. 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. The response includes fiatPaymentInstructions: for example a CVU in Argentina, a CLABE in Mexico or a PIX BR Code in Brazil.
on-ramp.ts
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.
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.For recurring top-ups, an On-Ramp Session 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. 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 and support ticket 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. Compute the HMAC over the raw bytes, never over re-serialized JSON.
verify-webhook.ts
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

balance.ts
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: 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. In Argentina, Brazil, Mexico and Colombia (breb), confirm the payout account with Confirm Fiat Account 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 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.
payout-account.ts
Create the off-ramp session once the account is ENABLED. See Create Off-Ramp Session. 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.
off-ramp-session.ts
Send wFiat from the Privy wallet to the session’s deposit address, on the same chain:
use-send-wars.ts
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. 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. There is nothing to refund.
Need a locked rate for a specific amount? Use Create Off-Ramp Order 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 to get customers to COMPLETED, and simulate the user’s fiat payment for an on-ramp with Simulate Deposit, using the value from fiatPaymentInstructions.

Moving wFiat across chains

The Ripio Bridge 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.

Resources

Questions about wFiat or the Ramps API? Write to [email protected].