Multi-currency

The /api/mca routes: your currency balances, their account details, rate quotes and conversions. Each currency is its own balance. A currency balance can be in US dollars too, and it's still separate from your USD business account. For how it fits together, see the multi-currency guide.

In sandbox

Balances, account details and activity work with test money. Live balances aren't part of our first launch. Converting is coming soon: it isn't switched on for beta accounts yet (features.convert is false), so quotes and conversions answer 422 capability_unavailable. Paying from a balance is coming soon too, and its routes answer the same.

Conventions

  • Two ways in. An Authorization: Bearer apk_… header means an API key: the route's scope must be on the key itself (an old * key gets 403). Without the header, the request uses your dashboard session. Routes marked dashboard only refuse an API key with 403 session_required, and their writes are for workspace owners and admins.
  • Amounts are whole minor units as strings (amountMinor: "10000" is EUR 100.00), never decimals. Balances carry their exponent.
  • Errors always carry error; most refusals also carry a plain message. Another workspace's ids answer 404.
  • Writes check availability first: a step that isn't available answers 422 capability_unavailable before anything is held or written. Balance and activity reads are never refused that way; account details and the rate estimate answer 422 capability_unavailable while multi-currency isn't connected.
  • Live (not switched on): opening a balance and converting need the workspace's business verification approved, else 403 kyb_required. Sandbox skips this check.
  • Rate limits: 429 rate_limited (with Retry-After).

Balances

GET /api/mca/accounts

Bearer apk_ · read:treasury · or dashboard session

Your currency balances, read from SorenPay's ledger.

FieldWhat it is
previewtrue only for a workspace that has never been set up while multi-currency isn't connected; the dashboard then shows sample details
kybBusiness verification for these balances
accounts[]One per currency: currency, status, balance (availableMinor, pendingMinor, exponent), scopeStatus and detailsStatus
coverage.currencies[]What this workspace can open: currency, receive, convert, and a reason when it can't hold that currency
featuresconvert, estimate and send: which balance actions your account has right now (left out when unknown)

scopeStatus is ok, checking or paused. While it isn't ok we're double-checking that balance with our partner, and it can't be converted from.

GET /api/mca/accounts/{currency}/details

Bearer apk_ · read:treasury · or dashboard session

The account details to get paid in that currency: {currency, status, details: [{rail, holderName, holderKind, status, fields}]}. Errors: 400 invalid_currency, 404 not_found, and 422 capability_unavailable in preview.

POST /api/mca/accounts/{currency}

Dashboard only (owner or admin) · Idempotent

Open a balance in a currency ("Add currency"). Idempotency-Key is required. Errors: 400 invalid_currency, idempotency_key_required, invalid_idempotency_key; 409 currency_account_exists; 422 currency_unavailable, currency_restricted, currency_not_eligible, capability_unavailable.

GET /api/mca/activity?currency=EUR

Bearer apk_ · read:treasury · or dashboard session

What happened in one currency balance: money in, conversions, charges and holds, as {data}. Error: 400 currency_required.

Onboarding

GET /api/mca/onboarding

Bearer apk_ · read:treasury · or dashboard session

{kybStatus, actionRequired, resubmissionAllowed, sandboxAutoAccepted}.

POST /api/mca/onboarding

Dashboard only (owner or admin)

Start or continue onboarding. In sandbox your workspace is set up and accepted in one call, with no verification step.

Quotes and conversions

POST /api/mca/quotes

Bearer apk_ · write:treasury · or dashboard session (owner or admin) · Idempotent

Lock a rate. Idempotency-Key is required. Both the sell and the buy balance must be open and ready before you quote: to buy a currency you don't hold yet, open its balance first. Buying USD credits a USD currency balance, not your USD business account.

{
"sellCurrency": "EUR",
"buyCurrency": "GBP",
"amountMinor": "10000",
"fixedSide": "sell"
}

fixedSide says which amount is fixed (sell by default). Answers 201: {id, status, sell, buy, rate, rateDisplay, includesOurFee, hold, lockExpiresAt, serverNow}. sell, buy and hold are {currency, amountMinor, display}. hold is what converting sets aside, which your balance must cover. Count the lock down from lockExpiresAt − serverNow.

Errors: 400 invalid_body, same_currency; 404 currency_account_not_found (you don't hold the sell or the buy currency); 409 account_not_ready (a balance is still being set up), onboarding_required (multi-currency isn't set up for your workspace yet); 422 capability_unavailable, currency_unavailable, account_suspended; 429 rate_limited (quotes per workspace per minute); 503 quote_lifetime_too_short.

POST /api/mca/conversions

Bearer apk_ · write:treasury · or dashboard session (owner or admin) · Idempotent

Convert at a quote's locked rate: {"quoteId": "…"}. Idempotency-Key is required. Answers 202 {orderId, status} with status held or provider_submitted: accepted, not finished.

Errors: 404 quote_not_found; 422 quote_used, quote_expired, insufficient_funds, balance_paused, limit_exceeded, account_suspended, capability_unavailable; live only: 403 kyb_required.

GET /api/mca/conversions/{id}

Bearer apk_ · read:treasury · or dashboard session

{id, status, label, sell, buy, createdAt}, with label in plain words. status is one of held, provider_submitted, outcome_unknown, completed, rejected, failed, cancelled or abandoned. Only completed means the money has moved; buy then shows the amount that arrived. Error: 404 not_found.

GET /api/mca/rates?from=EUR&to=GBP&amount=100.50

Dashboard only

An indicative estimate while someone types: {from, to, fixedSide, indicative: true, rate, rateDisplay, asOf, sell, buy}. Nothing is locked or held; converting still needs a quote. amount is a plain decimal in the fixed side's currency (leave it out for the rate of one unit), and fixedSide is sell (default) or buy.

Errors: 400 invalid_currency, invalid_amount; 422 with a currency_… code (a currency you can't use), same_currency, invalid_amount, rate_unavailable or capability_unavailable (multi-currency isn't connected).

Payouts from a balance

Coming soon

No partner serves payouts from a currency balance yet, so the two POST routes below answer 422 capability_unavailable and GET /api/mca/accounts reports features.send: false.

POST /api/mca/payouts/quotes

Dashboard only (owner or admin) · Idempotent

Lock the rate for paying a saved recipient in their currency from a balance: {counterpartyId, sourceCurrency, recipientCurrency, amountMinor}, where amountMinor is what the recipient gets.

POST /api/mca/payouts

Dashboard only (owner or admin) · Idempotent

Pay a saved recipient from a balance: {counterpartyId, sourceCurrency, recipientCurrency, amountMinor, quoteId?, reference?}.

GET /api/mca/payouts

Bearer apk_ · read:treasury · or dashboard session

Payouts from your currency balances, newest first (?status=in_progress for the ones still in progress): {data: [{id, status, inProgress, label, recipient, from, to, rate, createdAt}]}.

Sandbox only

Add test money is a button on a balance in the dashboard of a sandbox workspace (POST /api/dev/mca/simulate-payin, dashboard only). The test money arrives in the balance the way a real payment would: a little later, not in the response.