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.
- 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
expiresAtand 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
travelRuledeclaring who owns the wallet (20108otherwise): see Travel Rule (Mexico).Colombia: PSE and R2P orders need
extraData.callback, and PSE also needsextraData.bankCode(from List Banks).brebneeds an enabled deposit account and confirmed payout accounts.
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_idandclient_secret, sandbox or production). Register your company to get them.
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:PrivyProvider and have Privy create an EVM embedded wallet when the user logs in:
providers.tsx
use-embedded-address.ts
2. Set up the Ramps API on your backend
The Ramps API is served fromhttps://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
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
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
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.
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 separatekyc_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
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
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 withGET /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 alreadyUNCONFIRMED, with the resolved holder inmetadata. - Mexico (
bank_transfer) and Colombia (breb) validate it asynchronously: it starts asPROCESSINGand becomesUNCONFIRMEDlater. Poll List Fiat Accounts for the transition.
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
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
use-send-wars.ts
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.REFUNDEDarrives once it’s back on-chain (readrefundedTxnHashon the order for the hash).refundable: false: the wFiat was already sold and the fiat payout failed, usually withfailureReason: 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
PointRIPIO_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
- Ramps API introduction
- On-Ramp integration guide and Off-Ramp integration guide
- wFiat contracts and networks
- Ripio Bridge developer guide
- Privy React SDK setup