A user can hold accounts in more than one currency, for example USD and EUR. Each account keeps its own balance in its own currency. Money never changes currency on its own: a card top-up credits the account it was made to, and a payout debits the account it was made from. Moving money between currencies is an explicit exchange that you request.

What Your Client Can Do #

PayCA sets two things per client. Read them before you build the flow:

curl -s "$PAYCA_BASE_URL/v1/client/settings" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"
{
  "cardFundingCurrencies": ["EUR"],
  "exchanges": [
    { "from": "EUR", "to": "USD" }
  ]
}
Field Meaning
cardFundingCurrencies Currencies whose accounts can be topped up by card. An account in any other currency gets no card option from funding-options.
exchanges Currency pairs you may exchange, from the currency sold to the currency bought. Empty when exchange is not enabled for your client.

Both are configured by PayCA. Contact PayCA to change them.

Opening an Account in Another Currency #

Open the user's account in the currency you need, next to the account the user already has:

curl -s -X POST "$PAYCA_BASE_URL/v1/users/$USER_ID/accounts" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{ "currency": "EUR" }'
{
  "id": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12",
  "currency": "EUR",
  "balance": { "available": "0.00", "pending": "0.00" },
  "status": "active"
}
  • The call is safe to repeat: if the user already has an account in that currency, the same account is returned unchanged.
  • The currency must be one listed for your client in GET /v1/client/settings: in cardFundingCurrencies, or in one of the exchanges.
  • 400 means the currency is not a three-letter code or is not offered to your client. 404 means the user does not exist.
  • The new account works with every account endpoint, the same as any other account: balance, transactions, funding-options, deposits, payouts.

GET /v1/users/{id} keeps returning the user's original account in account. Accounts opened later are listed in otherAccounts:

{
  "id": "73f77d0f-8b30-46e8-9081-b506180f764e",
  "account": { "id": "5d0c…", "currency": "USD", "…": "…" },
  "otherAccounts": [
    { "id": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12", "currency": "EUR", "…": "…" }
  ]
}

otherAccounts is absent or empty when the user has no other accounts.

Topping Up and Paying Out in the Account's Currency #

For the complete EUR walkthrough, see EUR Card Top-ups and Payouts.

  • Top-up. Use the card deposit flow on the account itself: funding-options, deposit-quote and deposits with that account's id. The amount is in the account's currency, and the card is charged in it. See Card Deposits Into PayCA Accounts. On this kind of account, the card option usually has cardDetailsRequired: true: you collect the card and send the payer's full profile.
  • Payout. Use payout-options, payout-quote and payouts on the same account. The recipient is paid in the account's currency, and the account is debited in it. A card payout from such an account needs the account holder's full profile: first and last name, email, phone, date of birth, city, address, postal code and country. It also needs the ipAddress of the holder's device. Send what PayCA does not already hold in recipient. A request that misses any of it is refused and names the missing fields. Only Visa and Mastercard cards are paid out to.

Exchanging Between Accounts #

An exchange moves money from one account into another account of the same owner, in another currency. It is available only for the pairs listed in exchanges.

  1. Quote the exchange and show the result to the user.
  2. Create it with an Idempotency-Key and the quoted targetAmount as expectedTargetAmount.
  3. Record the returned exchange id. Both accounts' transactions carry it.

Step 1: Quote #

curl -s -X POST "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/exchange-quote" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -d '{
    "toAccountId": "'"$USD_ACCOUNT_ID"'",
    "amount": "100.00"
  }'

The path names the account sold from. amount is what leaves it, in its currency, fee included.

{
  "fromAccountId": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12",
  "toAccountId": "5d0c2a41-7e3b-4c8d-a1f6-2b9e8d7c6a50",
  "sourceAmount": "100.00",
  "sourceCurrency": "EUR",
  "fee": "0.80",
  "rate": "1.07415",
  "targetAmount": "106.55",
  "targetCurrency": "USD"
}
Field Meaning
sourceAmount Taken from the account sold from. The fee is part of it.
fee Exchange fee, in the currency sold.
rate Units of the currency bought per one unit of the currency sold. Your client's markup is already applied.
targetAmount Credited to the account bought into.

A quote reserves nothing. The rate is the market rate of the moment, and the exchange is priced again when you create it.

How the Price Is Worked Out #

fee          = sourceAmount × fee% + fixed fee      rounded UP to the sold currency's minor unit
rate         = market rate × (1 − markup%)
targetAmount = (sourceAmount − fee) × rate          rounded DOWN to the bought currency's minor unit

With a market rate of 1.0850, a 1% markup, a 0.5% fee and a fixed fee of EUR 0.30, EUR 100.00 gives a fee of EUR 0.80 and a rate of 1.07415. The account is credited USD 106.55 (99.20 × 1.07415 = 106.5557, rounded down).

The markup and the fee are set for your client by PayCA. Both may be zero. An amount with more decimals than the currency has, or one that the fee would consume entirely, is refused with 400.

Step 2: Create #

curl -s -X POST "$PAYCA_BASE_URL/v2/accounts/$EUR_ACCOUNT_ID/exchanges" \
  -H "Content-Type: application/json" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET" \
  -H "Idempotency-Key: exchange-20260929-0001" \
  -d '{
    "toAccountId": "'"$USD_ACCOUNT_ID"'",
    "amount": "100.00",
    "expectedTargetAmount": "106.55"
  }'

The response is the quote as applied, with the exchange's id and createdAt:

{
  "id": "e41a9c3b-6d2f-4b8e-9a17-5c0d3e2f1b84",
  "createdAt": "2026-09-29T12:04:31Z",
  "fromAccountId": "0b8f5c7e-2f4d-4a51-9b1e-3c6a7d9e0f12",
  "toAccountId": "5d0c2a41-7e3b-4c8d-a1f6-2b9e8d7c6a50",
  "sourceAmount": "100.00",
  "sourceCurrency": "EUR",
  "fee": "0.80",
  "rate": "1.07415",
  "targetAmount": "106.55",
  "targetCurrency": "USD"
}

The exchange is complete when the response arrives. Both balances have already changed.

  • expectedTargetAmount protects the user from a rate that moved after the quote. If the rate of the moment would credit less, the exchange is refused with 409 and nothing moves. Quote again and show the user the new amount. A rate that moved in the user's favour is applied as it is.
  • Retries. If the request times out, repeat it with the same Idempotency-Key and the same body. You get the original exchange back, at the price it was made. It is not made or priced a second time.

Which Accounts Can Be Exchanged #

  • Both accounts belong to the same owner, for example the same user.
  • They are two different active accounts, and their currency pair is listed in exchanges.
  • The account sold from holds at least sourceAmount available.

Exchange Errors #

Status Meaning Action
400 Invalid request, insufficient available balance, accounts that cannot be exchanged (different owners, the same account, an unsupported currency pair), an amount too small or with too many decimals, or no rate available right now. Fix the request. For a missing rate, retry later.
403 Exchange is not enabled for your client. Check GET /v1/client/settings. Contact PayCA to enable it.
404 An account was not found. Check both account ids.
409 The rate moved below expectedTargetAmount, or the Idempotency-Key was already used for a different exchange. Quote again, or use a new key for a new exchange.

Exchanges in Transactions and Webhooks #

An exchange appears as two account transactions with subtype currency_exchange. Both carry the exchange id as referenceId:

Account type/subtype amount
Sold from withdraw/currency_exchange sourceAmount, fee included
Bought into deposit/currency_exchange targetAmount

The fee is not a separate transaction on the user's accounts. It is the difference between sourceAmount and what was converted, and the exchange response reports it as fee. Group the two legs by referenceId. Compare them with the stored exchange response.

Integration Checklist #

  • Read GET /v1/client/settings to know which currencies can be topped up by card and which exchanges are enabled.
  • Open the second-currency account with POST /v1/users/{id}/accounts. It is safe to call again.
  • Read other accounts from otherAccounts. account stays the user's original account.
  • Keep balances per currency. Never add up amounts in different currencies.
  • Quote before every exchange and send expectedTargetAmount. Quote again on 409.
  • Use one Idempotency-Key per exchange, and reuse it on retries.
  • Reconcile both currency_exchange legs by referenceId.