Not what you’re looking for? Conversion rules handle asset exchange with automatic fund movement across rails. If
you need to move funds between accounts in the same asset without conversion, see
Transfers. To execute an institutional FX-style trade against a
locked quote, see Swap. For fiat-to-crypto purchases via a hosted checkout
UI, see Ramp.
How it works
A conversion rule is a persistentfrom → to configuration scoped to the selected account identified by the path’s {account_id}. When you create a rule, PWS provisions a dedicated inbound endpoint — a virtual IBAN for fiat, or a deposit address for crypto — and returns it on the rule’s from side. You share those payment instructions with your end customer. Every matching credit to that endpoint automatically triggers a conversion and settles the converted asset to the rule’s to destination.
Rules remain active and reusable as long as the account’s KYC profile is valid. For a single on-demand conversion, use the on-demand conversions API instead of creating a rule.
On-demand conversions
An on-demand conversion executes once for the selected account using the source and destination in the request. It does not create a reusable conversion rule. The conversion is processed asynchronously, so the create response returns its initial status. Use the get or list endpoint to monitor it until it completes or fails.{account_id} is the identifier of the selected account that owns the conversion. It is not the user identifier.
Create an on-demand conversion
Provide a source and a destination. The example below uses a balance source and a wallet destination. Set exactly one amount:from.amountfixes the amount debited from the account balance. The API calculates the wallet amount.to.amountfixes the amount delivered to the wallet. The API calculates the balance amount.
from and to fields. Each response includes the amount debited from the account, the amount delivered to the wallet, and the current settlement status for both sides. If the request supplies a memo or tag and it is saved, that field is returned in the create response and in later get and list responses:
Idempotency-Key is a UUID for the create request. It must be unique. If the key was already used, the API returns a conflict error and does not create a new conversion.
Monitor an on-demand conversion
UseGET /v1/accounts/{account_id}/on-demand-conversions/{conversion_id} for one conversion, or GET /v1/accounts/{account_id}/on-demand-conversions to list conversions. The list is ordered by created_at descending. It uses cursor pagination with a default page size of 20 and a maximum page size of 25.
The lifecycle status has these meanings:
Create, get, and list responses use the same conversion details.
from.status and to.status show the current settlement status for each side: pending means the side is still processing, held means it is waiting for review or release, settled means it completed successfully, and failed means it will not settle. The destination includes its network and wallet address. Destination-side fees and transaction references are included when available. A supplied destination memo or tag is returned in every response when it was saved.
The rate is an exchange rate. rate.base.symbol and rate.quote.symbol identify the rate pair, and rate.price is the price of one unit of the base asset denominated in the quote asset. For example, a price of 0.0004 with USD as the base and ETH as the quote means one USD converts to 0.0004 ETH.
Creating a rule — fiat on-ramp (EUR → USDC on Polygon)
An end customer sends EUR via SEPA and automatically receives USDC on Polygon at the destination wallet you specify. On thefrom (source) side you supply only symbol, type, and the rail (bank_account.via / wallet.via). The inbound routing is provisioned by PWS and returned on the response — any routing fields you send on from are ignored.
from.bank_account with the provisioned virtual IBAN and BIC:
from.bank_account.iban and from.bank_account.bic with the end customer as their payment instructions. Every SEPA credit to that IBAN fires the rule and produces a new conversion transaction.
Provisioning: For some fiat rails the inbound account is provisioned asynchronously. The rule is returned with
status: provisioning and the from routing fields are populated once provisioning completes (the rule then moves to
active). Fetch the rule with GET /v1/accounts/{account_id}/conversions/{conversion_rule_id} to read the final
routing.Creating a rule — crypto off-ramp (USDC on Ethereum → EUR via SEPA)
An end customer sends USDC on Ethereum and automatically receives EUR via SEPA to the bank account you specify.from.wallet.address carries the provisioned Ethereum deposit address. Share it with the end customer — any USDC deposit to that address triggers the rule and initiates the SEPA payout.
Fiat destinations: The beneficiary on a fiat destination is the account’s KYC identity — third-party bank accounts
are not permitted. The beneficiary is derived from KYC, so there is no beneficiary field on the request.
Supported rails and networks
Bank destinations specify abank_account.via and the routing fields that rail requires; crypto destinations specify a
wallet.via (network) and address:
The optional
bank field on a destination named by routing details is the name of the beneficiary’s financial
institution; it is passed through to the underlying provider. A destination naming a linked account ignores it —
the bank is already known from the link.
The conversion rules API supports sepa and fps for bank destinations named by routing details. An unsupported via returns 400.
Naming a bank account by its routing details is limited to SEPA and FPS. Other rails named this way, including
Fedwire, are rejected with
400.US bank destinations
A US bank account is reached a different way: link it once, then name it by its identifier instead of its routing details. Sendtype: "bank", symbol: "USD", the rail in bank_account.via (ach or rtp), and the
account_link_id of a linked account inside that same bank_account block.
via chooses which one the payout uses. Leaving it out is a
400, and any value other than ach or rtp is rejected.
See Bank links for how to link an account and get that identifier.
The two shapes are mutually exclusive: a bank_account carries either routing details or an account_link_id, never both.
Crypto destination tags and memos
Networks that need a destination tag or memo to route a deposit accept them as optional fields on the wallet destination — the XRP destinationtag, or the memo used by chains such as XLM, EOS, and Cosmos:
tag / memo for networks that don’t use them.
Multi-network support
A rule matches a single asset on a single network. Accepting the same asset on multiple networks requires one rule per network. Each rule gets its own provisioned inbound address and can be paused or deleted independently — keeping reconciliation and per-network reporting straightforward. To accept USDC on Ethereum and Polygon, create two rules identical except forfrom.wallet.via:
Rule lifecycle
Deleting a rule is a soft delete: after
DELETE /v1/accounts/{account_id}/conversions/{conversion_rule_id}, the rule no longer fires and a subsequent GET returns 404 Not Found. Deleted rules are never returned by listConversionRules.
Managing a rule
Update a rule’s destination, label, or status withPUT /v1/accounts/{account_id}/conversions/{conversion_rule_id}. This is a full-state replacement of to, name, and status — the source side (from) is immutable, so re-create the rule to change input symbol, rail, or source type. status must be active or paused.
Monitoring conversions
Each firing of a rule is a conversion transaction. List the transactions for a rule withGET /v1/accounts/{account_id}/conversions/{conversion_rule_id}/transactions:
Webhooks. Subscribe to conversion webhook events for real-time updates instead of polling — register your endpoint
with the Register Webhook API. See Conversion
webhooks.
listConversionRules supports only status, page_size, and page_token (cursor) parameters. Use the per-rule transactions endpoint above for transaction history.
Conversion webhooks
Conversion lifecycle webhook payloads include optional enrichment fields. They are omitted when the source event does not carry the corresponding fact.error_code, when present, is one of: amount_too_small, quote_expired, destination_rejected, wallet_not_whitelisted, wallet_verification_method_not_supported, travel_rule_data_missing, or unknown.
destination_address is the crypto destination only. It is never a fiat destination, tag, or memo.
chain_references.blockchain_transaction_id is the on-chain transaction id or hash for a completed crypto withdrawal. It is distinct from withdraw_txid.
Resolving a Travel Rule block
conversion.travel_rule_blocked means a pre-trade Travel Rule check stopped the conversion before any quote was executed, because the destination wallet needs address-ownership verification (or the check could not be evaluated). The error_code tells you how to resolve it:
wallet_not_whitelisted— the destination address is not yet ownership-verified. Whitelist it withPOST /v1/users/{user_id}/travel-rule/verifications(hosted declaration or self-attestation); once the address is verified, subsequent conversions to it will proceed.wallet_verification_method_not_supported— the address requires Satoshi-test or digital-signature verification, which is not available via the API yet. Contact Support to resolve.travel_rule_data_missing— eligibility could not be evaluated because required account information (for example, the user’s country) is missing. Complete the user’s profile and retry; contact Support if the block persists.
Whitelisting external wallets
Before a conversion can pay out to an external crypto address, that address must pass a Travel Rule address-ownership check. Whitelist it withPOST /v1/users/{user_id}/travel-rule/verifications: use method: hosted_declaration when the wallet is held by another custodial service, or method: self_attestation (supplying the end user’s ownership evidence) when the end user controls the wallet.
Error handling
Missing or invalid off-ramp bank details
When you create or update an off-ramp rule (crypto → fiat), theto.bank_account fields are validated against the chosen rail before the rule goes live. If a field the rail requires is missing or malformed, the request is rejected with 400 Bad Request and code: conversion_invalid_withdrawal_address. Each causes[] entry names the offending field with a to.bank_account.* path so you know exactly which value to supply.
bic. Populate every field the rejection names and resubmit.
API reference
Conversion rules: The following are planned but not exposed by the API today: a fee breakdown / executed rate /
swap-quote on rules, a structured
failure_reason, and an expires_at on inbound deposit addresses. On-demand
conversion responses have a separate rate field and may include fees and transaction references; see On-demand
conversions.