FX Swaps
Swap between stablecoins of different currencies using a two-step estimate and execute flow.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain access tokens |
| Onboarding | User and wallet registration |
| Unified Balance | Unified tokens and balance model |
Overview
FX swaps convert between stablecoins denominated in different currencies — USDC to EURC, for example.
The swap is executed by Circle, not on-chain by the user's wallet, so the flow is not a single
transaction. It has three parts:
- Estimate. You ask for a quote. Wirex returns a rate, an
estimation_id, a deposit address, and
an expiry. - Pay. You send the source tokens to the deposit address from the user's wallet, on-chain,
yourself. Wirex does not move the funds for you. - Execute. You hand back the
estimation_idand the hash of the payment transaction. Wirex
settles the swap and returns atrade_id.
Between steps 2 and 3 the funds are in transit and the quote is expiring. Handle that window
explicitly — see Limitations.
Prerequisites
| Requirement | Detail |
|---|---|
StableExchange capability | Must be Active on the user. Check via GET /api/v1/capabilities — see Capabilities |
| Both tokens supported | Source and destination contract addresses must both appear in GET /api/v1/config/tokens |
| Funded wallet | The user must hold enough of the source token to make the on-chain payment |
If the capability is not Active, both endpoints fail before any quote is produced. Read
status_reason from the capabilities response to find out why — the reasons are listed in
Diagnosing a Blocked Capability.
Step 1: Estimate the Swap
POST /api/v1/fx/estimate
Quote in one direction only. Send either source_asset_amount ("swap 100.50 USDC into whatever
that buys") or destination_asset_amount ("buy me exactly 95.25 EURC"), never both and never
neither.
Request
{
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"source_asset_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"source_asset_amount": "100.50",
"destination_asset_address": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c"
}| Field | Type | Required | Description |
|---|---|---|---|
correlation_id | string | No | Client-supplied tracking identifier. Generated server-side if omitted |
source_asset_address | string | Yes | Contract address of the token to swap from. Must be non-zero, supported, and a regular token — see Limitations |
source_asset_amount | string | Conditional | Decimal amount to swap. Required unless destination_asset_amount is sent |
destination_asset_address | string | Yes | Contract address of the token to receive. Must be non-zero and supported |
destination_asset_amount | string | Conditional | Decimal amount to receive. Required unless source_asset_amount is sent |
Amounts are decimal strings, not integers in the token's smallest unit — "100.50", not
"100500000". They must parse as a positive number; zero and negative values are refused.
Response
{
"estimation_id": "550e8400-e29b-41d4-a716-446655440000",
"source_asset_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"source_asset_amount": "100.50",
"destination_asset_address": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c",
"destination_asset_amount": "95.25",
"trade_rate": "0.952500",
"destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"deposit_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"expiration_time": "2024-01-15T10:30:00Z"
}| Field | Description |
|---|---|
estimation_id | Identifier for this quote. Pass to POST /api/v1/fx in step 3 |
source_asset_address | Echo of the requested source token |
source_asset_amount | Amount of source token to send, formatted to 2 decimal places |
destination_asset_address | Echo of the requested destination token |
destination_asset_amount | Amount of destination token to be received, formatted to 2 decimal places |
trade_rate | Exchange rate applied to this quote |
destination_address | Address to send the source tokens to |
deposit_address | Deprecated. Mirrors destination_address for backward compatibility. Read destination_address |
expiration_time | RFC 3339 timestamp after which the quote is no longer valid |
Both amount fields are formatted to exactly two decimal places regardless of the token's own
precision. Treat them as display values. When you quoted by destination_asset_amount, the
source_asset_amount shown has been rounded for presentation — do not derive an on-chain transfer
amount from it by parsing it back at full precision.
Step 2: Pay the Deposit Address
Send source_asset_amount of the source token to destination_address from the user's wallet. This
is an ordinary on-chain ERC-20 transfer that your integration submits; there is no Wirex endpoint for
it. Keep the transaction hash — step 3 requires it.
Wait for the transaction to be mined before continuing. Submitting an unmined or reverted hash to
step 3 fails the swap.
Step 3: Execute the Swap
POST /api/v1/fx
Request
{
"estimation_id": "550e8400-e29b-41d4-a716-446655440000",
"payment_transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}| Field | Type | Required | Description |
|---|---|---|---|
estimation_id | string | Yes | The estimation_id from step 1. Must be a valid UUID |
payment_transaction_hash | string | Yes | Hash of the payment transaction. 0x-prefixed, exactly 66 characters |
Response
{
"trade_id": "550e8400-e29b-41d4-a716-446655440000"
}| Field | Description |
|---|---|
trade_id | Identifier for the executed trade |
Code Examples
// Step 1: estimate
const estimate = await fetch(`${baseUrl}/api/v1/fx/estimate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Wallet': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
source_asset_address: sourceTokenAddress,
source_asset_amount: '100.50',
destination_asset_address: destinationTokenAddress
})
}).then(r => r.json());
// Step 2: pay destination_address on-chain, then take the hash
const paymentTransactionHash = await sendTokens(
estimate.destination_address,
estimate.source_asset_amount
);
// Step 3: execute
const trade = await fetch(`${baseUrl}/api/v1/fx`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Wallet': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
},
body: JSON.stringify({
estimation_id: estimate.estimation_id,
payment_transaction_hash: paymentTransactionHash
})
}).then(r => r.json());headers = {
"Authorization": f"Bearer {access_token}",
"X-User-Wallet": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json"
}
# Step 1: estimate
estimate = requests.post(
f"{base_url}/api/v1/fx/estimate",
headers=headers,
json={
"source_asset_address": source_token_address,
"source_asset_amount": "100.50",
"destination_asset_address": destination_token_address
}
).json()
# Step 2: pay destination_address on-chain, then take the hash
payment_transaction_hash = send_tokens(
estimate["destination_address"],
estimate["source_asset_amount"]
)
# Step 3: execute
trade = requests.post(
f"{base_url}/api/v1/fx",
headers=headers,
json={
"estimation_id": estimate["estimation_id"],
"payment_transaction_hash": payment_transaction_hash
}
).json()// Step 1: estimate
estimateBody, _ := json.Marshal(map[string]string{
"source_asset_address": sourceTokenAddress,
"source_asset_amount": "100.50",
"destination_asset_address": destinationTokenAddress,
})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/fx/estimate", bytes.NewReader(estimateBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var estimate FxEstimateResponse
json.NewDecoder(resp.Body).Decode(&estimate)
// Step 2: pay estimate.DestinationAddress on-chain, then take the hash
paymentTransactionHash := sendTokens(estimate.DestinationAddress, estimate.SourceAssetAmount)
// Step 3: execute
executeBody, _ := json.Marshal(map[string]string{
"estimation_id": estimate.EstimationId,
"payment_transaction_hash": paymentTransactionHash,
})
req, _ = http.NewRequest("POST", baseURL+"/api/v1/fx", bytes.NewReader(executeBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
resp, _ = http.DefaultClient.Do(req)
defer resp.Body.Close()
var trade FxExecuteResponse
json.NewDecoder(resp.Body).Decode(&trade)Limitations
- The quote expires.
expiration_timeis a hard deadline that includes the time your on-chain
payment takes to confirm. Budget for confirmation before accepting a quote, not after. - The payment leg is yours. Wirex issues the quote and settles the swap, but does not move funds
to the deposit address. A failed or under-funded transfer leaves an estimation that will never be
executable. - One direction per quote. An estimate fixes either the source or the destination amount. To
change which side is fixed, request a new estimate. - Response amounts are 2-decimal display values, not full-precision figures.
deposit_addressis deprecated. It currently carries the same value asdestination_address.
Readdestination_address.- Both tokens must be in the catalogue. An address absent from
GET /api/v1/config/tokensis
refused at the estimate. - FX operates on regular tokens. A user holding the unified WUSD or WEUR must unwrap before
funding the swap —destination_addressexpects the regular token named by
source_asset_address(USDC, USDT, EURC), so the funding transfer has to unwrap and send the
underlying token. Depositing a unified token is not matched to the estimate.
Push-to-card does not share this constraint.
Error Handling
Validation Errors (400)
| Error Details | Description | Resolution |
|---|---|---|
amount: Either source_asset_amount or destination_asset_amount must be provided | Neither amount sent | Send exactly one |
amount: Only one of source_asset_amount or destination_asset_amount can be provided | Both amounts sent | Send exactly one |
source_asset_address: Source asset address must be a valid non-zero contract address | Missing or zero address | Send a valid contract address |
destination_asset_address: Destination asset address must be a valid non-zero contract address | Missing or zero address | Send a valid contract address |
source_asset_amount: Amount must be a positive decimal string | Non-numeric, zero or negative | Send a positive decimal string |
destination_asset_amount: Amount must be a positive decimal string | Non-numeric, zero or negative | Send a positive decimal string |
source_asset_address: Source token not found or not supported | Token is not in the catalogue | Check GET /api/v1/config/tokens |
destination_asset_address: Destination token not found or not supported | Token is not in the catalogue | Check GET /api/v1/config/tokens |
estimation_id: Estimation ID is required | Field empty on execute | Send the estimation_id from the estimate |
estimation_id: Estimation ID must be a valid UUID | Malformed identifier | Send the value verbatim from the estimate |
payment_transaction_hash: Transaction hash must be 0x-prefixed | Missing 0x prefix | Prefix the hash |
payment_transaction_hash: Transaction hash must be 66 characters (0x + 64 hex characters) | Wrong length | Send the full 32-byte hash |
payment_transaction_hash: Transaction hash contains invalid hex characters | Non-hex characters present | Send a hex-encoded hash |
user: Requested user account not found or does not belong to you | Identity headers do not resolve to a user | Check X-User-Wallet matches the token |
Example Error Response
{
"error_reason": "ErrorInvalidField",
"error_description": "Request failed validation",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "amount", "details": "Only one of source_asset_amount or destination_asset_amount can be provided" }
]
}Capability Errors
A user without an Active StableExchange capability is refused on both endpoints. The failure
surfaces as a capability error rather than a field error — resolve it through
Capabilities, not by changing the request.
Updated 20 days ago

