Swapped
@openrampkit/adapter-swapped is a fiat onramp and offramp through the Swapped widget, shown in an iframe. Deposits cover cards, Apple Pay, Google Pay, SEPA, and many local methods in Southeast Asia and beyond. Withdrawals pay out to a bank transfer, Skrill, PIX or Interac.
import { swapped } from '@openrampkit/adapter-swapped'
swapped({
publicKey: process.env.SWAPPED_PUBLIC_KEY!, // pk_...
secretKey: process.env.SWAPPED_SECRET_KEY!, // sk_...
env: 'production',
})To get the keys, see Get provider keys.
Options
| Option | Type | Default | Description |
|---|---|---|---|
publicKey | string | required | Public key (pk_...). Used as apiKey in the widget URL and the merchant APIs. |
secretKey | string | required | Secret key (sk_...). Signs widget URLs and verifies order notifications. |
env | 'sandbox' | 'production' | 'production' | Sandbox uses https://sandbox.swapped.com (BTC and ETH testnets and test cards only). The server checks env against livemode. |
widgetUrl | string | https://widget.swapped.com or the sandbox URL | Widget base URL |
apiUrl | string | same as widgetUrl | Merchant API base URL |
markup | number | none | Your markup in percent, 0 to 5 (0.5 means 0.5%) |
deliverAssets | SwappedDeliverAsset[] | USDC on Base, Arbitrum, Polygon, Ethereum | Assets Swapped may deliver, most preferred first. A destination token that is not in the list gets no quote (NO_QUOTES). |
defaultCountry | string | 'US' | Country for pricing when the session has none |
statusPolling | boolean | false | Also poll get_transactions for order status. TO VERIFY (see below). |
SwappedDeliverAsset is { chain, token, currencyCode, symbol?, decimals? }, where currencyCode is Swapped's code such as USDC_BASE.
Legs
The adapter has one leg per Swapped payment_group. The leg id is the group name (creditcard, apple-pay, vietqr, ...). The method id is mapped to the OpenRampKit vocabulary:
| Swapped group | Method |
|---|---|
creditcard | card |
apple-pay, applepay | apple_pay |
google-pay, googlepay | google_pay |
bank-transfer, banktransfer | bank_transfer |
zalo, zalopay | zalopay |
mobile-money | mobile_money |
sepa, vietqr, momo, gcash, maya, gopay, dana, ovo, grabpay, promptpay, touchngo, pix, upi, blik, spei, astropay | same name |
Other groups keep their own name as the method id.
Groups outside Southeast Asia in the live list (get_payment_methods, 2026-10-04): blik (PL, PLN), spei (MX, MXN), mobile-money (KE in KES, ZM in ZMW) and astropay (31 countries, USD). These come from the live catalog only.
- Static legs:
creditcard,apple-pay,google-pay, used when the catalog is not available. - Live catalog:
GET /api/v1/merchant/get_payment_methods, cached for one hour. For the user's country and currency, it builds one leg per group, with the countries where it exists and the min and max amounts (in EUR). - Delivers to: the
deliverAssets(USDC on Base first by default). For other destinations, the planner adds a Relay bridge hop. - Regions: every country except
US-TX("users from Texas won't be able to use stablecoins"). - Surface:
IFRAME(560 px high). It declaresmessages: { completed: ['SWAPPED_ORDER_DATA'] }, so when the widget posts that message the modal checks the status at once (see Surfaces).
Sell legs (withdraw to cash)
For withdraw sessions, the adapter declares crypto_offramp legs. Their ids have the prefix sell-.
| Leg | Method | Payout currencies | Countries |
|---|---|---|---|
sell-bank-transfer | bank_transfer | EUR, DKK, GBP | all |
sell-skrill | skrill | EUR, DKK, GBP | all |
sell-pix | pix | BRL | BR |
sell-interac-extra | interac | CAD | CA |
- Static legs: the table above, from the Swapped docs (2026-09). The server uses them when the live catalog is not available.
- Live catalog: for a withdraw session,
catalog()callsGET /api/v1/merchant/sell/get_payout_methods(cached for one hour). It returns one sell leg per payout method that is enabled and pays out in the target currency, for the user's country and for all countries. Its min and max amounts become the leg limits (in EUR). - From: the
deliverAssets(USDC on Base, Arbitrum, Polygon, Ethereum by default), in the user's wallet or at the app's address. So the sessionsourcemust be one of them. - To: fiat in the user's own account.
- Regions: the payout method's countries, minus
US-TX. - Surfaces:
IFRAME(the Swapped sell widget), thenWALLET_TX(the crypto transfer to Swapped).
Sell quotes are estimates
Swapped prices a sell by the fiat amount only. The user enters a crypto amount, so the adapter makes two calls to POST /api/v1/merchant/sell/pricing:
- It prices 100 units of the payout currency, to get the fiat value of one crypto unit.
- It prices the fiat amount that the crypto amount buys (rounded down to cents).
The quote output is the fiat amount after fees. The fees are the Swapped fee (provider) and your markup (app), in the payout currency, with included: true. The quote expires after 10 minutes (quoteExpiresAt(10)). The widget sets the final amount, so the guarantee is estimate. Swapped does not report the payout amount, so result.outputConfirmed stays false.
Sell flow
- Start: a signed widget URL on
/sell, withuserSendsFunds=false, the payout method, the crypto code and amount, the payout currency,externalCustomerId, the email and country when known, andresponseUrl(the webhook URL). The user enters the payout details, and passes any checks that Swapped needs, in the widget. payment_pendingwebhook: Swapped is ready for the crypto. The notification has the deposit address (order_crypto_address) and the amount. The adapter turns it into aWALLET_TXstep: an ERC-20transferof that amount to that address. The user's wallet signs it, or the server's treasury hook sends it (custody: 'app').submit_tx: the client (or the server, for the treasury) reports the hash. The leg isprocessing(detail codeconfirming). The hash is thesourcetransaction of the leg.- Payout:
payout_pendingkeeps itprocessing(detail codesettling).order_completedissucceeded.order_cancelledfails the leg withPAYMENT_FAILED("The payout was cancelled.",recovery: 'contact_support').
The deposit address and the amount of the WALLET_TX come from the verified Swapped webhook, not from the quote. With custody: 'app', check them in your treasury hook.
Quotes and start
- Quote:
POST /api/v1/merchant/pricingwith the public key, the payment group, the fiat amount (or crypto amount), the target currency code and the region. Fees, in the fiat currency, withincluded: true: the Swapped fee (provider), the network fee (network) and your markup (app). The guarantee isestimate: the widget shows the final price. Quotes expire after 10 minutes (quoteExpiresAt(10)). - Start: a signed widget URL. The signature is the base64 HMAC-SHA256 of the query string (with the leading
?) using the secret key, appended last as&signature=. The URL locks the amount (lockBaseCurrency=true), sets the wallet address, the method, the email and country when known,redirectUrl(the return page) andresponseUrl(the webhook URL). - Reference:
externalCustomerIdis set to{userId}.{random}. Swapped echoes only this field in callbacks, so it routes the webhook to the leg. The user id stays visible as the prefix in the Swapped dashboard. providerRef: the Swappedorder_id, from the first order notification.
Webhooks
Swapped sends order notifications (buy and sell) to the responseUrl that the adapter puts in the widget URL: {baseUrl}/webhooks/swapped. You do not need to configure it in a dashboard.
- Verification: the
signatureheader must equal the base64 HMAC-SHA256 of the raw body with the secret key. An emptysecretKeyrefuses every notification. The signature has no timestamp. - Replay protection: the adapter gives the SHA-256 of the raw body as the replay key (
webhook.replayKey). The server keeps each key for 7 days in the adapter's shared store (claimWebhook, built onclaimOnce). A repeat of the same body in that time gets200with{ "received": true, "duplicate": true }and changes nothing. When the server cannot apply the event yet (it answers503), it gives the key back, so the provider's retry still applies. The key is also the event id (eventId), so a session drops the same event twice. - Status mapping for buy orders (sell orders: see Sell flow):
order_status | Leg |
|---|---|
order_broadcasted | succeeded, with the transaction id as the destination transaction |
order_completed | processing, detail code settling (paid and bought, not yet sent on chain) |
order_cancelled | failed with PAYMENT_FAILED |
payment_pending | no change |
| other | no change: the adapter logs the unknown status once (statusMap), and the leg keeps its step |
For a sell order, the transaction_id is the user's crypto transfer to Swapped: the source transaction.
Verified vs TO VERIFY
- The live payment-methods API returns
{ data: { [country]: Method[] } }. The docs show a flat list. The adapter accepts both. - TO VERIFY:
statusPolling. Theget_transactionssignature is assumed to be the base64 HMAC-SHA256 of the JSON body withoutsignature. It was not tested against the live API.