Skip to main content
The conversions API supports two patterns: reusable conversion rules that react to matching deposits, and one-off on-demand conversions. It is designed for partners building wallet-native or payments products who want to offer their users a seamless on- and off-ramp experience — without relying on card widgets or assembling multiple providers for KYC, rails, conversion, and settlement. The core primitive is a conversion rule: a persistent per-account configuration that provisions a dedicated inbound endpoint (a named virtual bank account for fiat deposits, or a deposit address for crypto deposits) and automatically converts and settles any matching deposit to the destination you have defined. Each time a matching deposit lands, the rule fires and produces a conversion transaction.
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 persistent fromto 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.amount fixes the amount debited from the account balance. The API calculates the wallet amount.
  • to.amount fixes the amount delivered to the wallet. The API calculates the balance amount.
Amounts are decimal strings. Requests that provide both amounts or neither amount are rejected.
Create, get, and list responses use the same 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:
The required 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

Use GET /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 the from (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.
The response is the created rule. PWS populates from.bank_account with the provisioned virtual IBAN and BIC:
Share 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.
On the response, 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 a bank_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. Send type: "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.
One link can be eligible for more than one rail, so 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 destination tag, or the memo used by chains such as XLM, EOS, and Cosmos:
Omit 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 for from.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 with PUT /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 with GET /v1/accounts/{account_id}/conversions/{conversion_rule_id}/transactions:
A transaction moves through the following states:
Webhooks. Subscribe to conversion webhook events for real-time updates instead of polling — register your endpoint with the Register Webhook API. See Conversion webhooks.
You can also list an account’s rules and filter by lifecycle state:
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 with POST /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 with POST /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.
Satoshi-test and digital-signature verification are not available via the API yet; contact Support for addresses that require them.

Error handling

Missing or invalid off-ramp bank details

When you create or update an off-ramp rule (crypto → fiat), the to.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.
The exact set of required fields depends on the rail and the provider routing the payout. Fields listed as optional in Supported rails and networks may still be required for a specific route — for example, some SEPA payout providers require 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.