The REasy API lets you send cross-border payouts from your own systems: get a quote, create a transfer, confirm it, and track it until the money is paid out. This page walks you through your first transfer.
Authentication
Every request must carry your organization's API key in the x-api-key header:
GET /public/balances HTTP/1.1
Host: api.reasy.fr
x-api-key: YOUR_API_KEY
- REasy issues one API key per organization and per environment. Ask your REasy contact if you don't have one.
- Treat it like a password: it grants full access to your account and it is also the secret used to sign the webhooks we send you (see Webhooks).
- A missing or invalid key returns
403.
How a transfer works
- Discover corridors —
GET /payout-corridorslists the destinations enabled for your organization (currency, country, delivery method, limits) and the fields each one requires. - Get a quote —
POST /ratesreturns a quotation with the exchange rate, fees and anexpiredAttimestamp. Quotes are short-lived; create the transfer before it expires. - Create the transfer —
POST /transferswith thequotationId, your ownexternalId, the sender/receiver details and the payout details. The send amount plus fees moves from your available to your pending balance. - Confirm —
POST /transfers/{id}/confirmsubmits the payout. Until you confirm, nothing is sent. - Track — we
POSTthe final status to yourcallbackUrl(paidorfailed), and you can alwaysGET /transfers/{id}.
Your first transfer
The examples below send a B2B bank transfer from XAF to CNY. Replace YOUR_API_KEY, the corridor id and the party details with your own.
JavaScript (Node.js 18+)
const BASE_URL = "https://<base_url>/";
const API_KEY = process.env.REASY_API_KEY;
async function reasy(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok || data?.errorCode) {
throw new Error(`REasy ${method} ${path} failed: ${JSON.stringify(data)}`);
}
return data;
}
// 1. Find a corridor you can use
const corridors = await reasy("GET", "/payout-corridors?currency=CNY&transactionMode=B2B&deliveryMethod=bankTransfer");
const corridor = corridors[0];
// 2. Get a quote (send 1,000,000 XAF)
const quote = await reasy("POST", "/rates", {
externalId: "quote-10042", // unique on your side
sourceCurrency: "XAF",
targetCurrency: corridor.targetCurrency,
transactionMode: "B2B",
payoutCorridorId: corridor.id,
mode: "SOURCE_AMOUNT",
sourceAmount: 1000000,
});
console.log(`Quote ${quote.id}: ${quote.sourceAmount} XAF -> ${quote.destinationAmount} CNY, fee ${quote.feeAmount} ${quote.feeCurrency}, expires ${quote.expiredAt}`);
// 3. Create the transfer
const transfer = await reasy("POST", "/transfers", {
quotationId: quote.id,
externalId: "order-10042", // unique on your side
callbackUrl: "https://example.com/webhooks/reasy",
purposeOfRemittance: "EXPORTED_GOODS",
documentReferenceNumber: "INV-10042",
sendingBusiness: {
registeredName: "Acme Cameroun SARL",
registrationNumber: "RC/DLA/2015/B/1234",
address: "Rue de la Joie, Akwa",
city: "Douala",
countryIsoCode: "CMR",
msisdn: "+237690000000",
},
receivingBusiness: {
registeredName: "Pingyao Shanghai Co., Ltd.",
address: "No. 1888, Century Avenue, Pudong New Area",
city: "Shanghai",
countryIsoCode: "CHN",
},
deliveryMethodInfo: {
bankAccountHolderFullName: "Pingyao Shanghai Co., Ltd.",
bankAccountNumber: "6222020200044445555",
bankAccountSwiftCode: "ICBKCNBJ",
},
});
console.log(`Transfer ${transfer.id} is ${transfer.status}`);
// 3b. Bank transfers only: upload the supporting invoice before confirming
await fetch(`${BASE_URL}/transfers/${transfer.id}/attachment`, {
method: "PUT",
headers: { "x-api-key": API_KEY, "content-type": "application/pdf" },
body: await import("node:fs/promises").then((fs) => fs.readFile("invoice-10042.pdf")),
});
// 4. Confirm — this submits the payout
await reasy("POST", `/transfers/${transfer.id}/confirm`);
// 5. Track it (you will also receive a webhook when it is paid or failed)
const status = await reasy("GET", `/transfers/${transfer.id}`);
console.log(`Transfer ${status.id}: ${status.status} (${status.statusCode})`);
PHP (8.1+)
<?php
const BASE_URL = 'https://<base_url>/';
$apiKey = getenv('REASY_API_KEY');
function reasy(string $method, string $path, ?array $body = null): mixed
{
global $apiKey;
$ch = curl_init(BASE_URL . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["x-api-key: $apiKey", 'content-type: application/json'],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode($raw, true);
if ($status >= 400 || (is_array($data) && isset($data['errorCode']))) {
throw new RuntimeException("REasy $method $path failed: $raw");
}
return $data;
}
// 1. Find a corridor you can use
$corridors = reasy('GET', '/payout-corridors?currency=CNY&transactionMode=B2B&deliveryMethod=bankTransfer');
$corridor = $corridors[0];
// 2. Get a quote (send 1,000,000 XAF)
$quote = reasy('POST', '/rates', [
'externalId' => 'quote-10042', // unique on your side
'sourceCurrency' => 'XAF',
'targetCurrency' => $corridor['targetCurrency'],
'transactionMode' => 'B2B',
'payoutCorridorId' => $corridor['id'],
'mode' => 'SOURCE_AMOUNT',
'sourceAmount' => 1000000,
]);
echo "Quote {$quote['id']}: {$quote['sourceAmount']} XAF -> {$quote['destinationAmount']} CNY, fee {$quote['feeAmount']} {$quote['feeCurrency']}, expires {$quote['expiredAt']}\n";
// 3. Create the transfer
$transfer = reasy('POST', '/transfers', [
'quotationId' => $quote['id'],
'externalId' => 'order-10042', // unique on your side
'callbackUrl' => 'https://example.com/webhooks/reasy',
'purposeOfRemittance' => 'EXPORTED_GOODS',
'documentReferenceNumber' => 'INV-10042',
'sendingBusiness' => [
'registeredName' => 'Acme Cameroun SARL',
'registrationNumber' => 'RC/DLA/2015/B/1234',
'address' => 'Rue de la Joie, Akwa',
'city' => 'Douala',
'countryIsoCode' => 'CMR',
'msisdn' => '+237690000000',
],
'receivingBusiness' => [
'registeredName' => 'Pingyao Shanghai Co., Ltd.',
'address' => 'No. 1888, Century Avenue, Pudong New Area',
'city' => 'Shanghai',
'countryIsoCode' => 'CHN',
],
'deliveryMethodInfo' => [
'bankAccountHolderFullName' => 'Pingyao Shanghai Co., Ltd.',
'bankAccountNumber' => '6222020200044445555',
'bankAccountSwiftCode' => 'ICBKCNBJ',
],
]);
echo "Transfer {$transfer['id']} is {$transfer['status']}\n";
// 3b. Bank transfers only: upload the supporting invoice before confirming
$ch = curl_init(BASE_URL . "/transfers/{$transfer['id']}/attachment");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["x-api-key: $apiKey", 'content-type: application/pdf'],
CURLOPT_POSTFIELDS => file_get_contents('invoice-10042.pdf'),
]);
curl_exec($ch);
curl_close($ch);
// 4. Confirm — this submits the payout
reasy('POST', "/transfers/{$transfer['id']}/confirm");
// 5. Track it (you will also receive a webhook when it is paid or failed)
$status = reasy('GET', "/transfers/{$transfer['id']}");
echo "Transfer {$status['id']}: {$status['status']} ({$status['statusCode']})\n";
Python (3.9+, requests)
requests)import os
import requests
BASE_URL = "https://<base_url>/"
session = requests.Session()
session.headers["x-api-key"] = os.environ["REASY_API_KEY"]
def reasy(method: str, path: str, body: dict | None = None):
res = session.request(method, f"{BASE_URL}{path}", json=body)
data = res.json() if res.content else None
if not res.ok or (isinstance(data, dict) and "errorCode" in data):
raise RuntimeError(f"REasy {method} {path} failed: {res.status_code} {data}")
return data
# 1. Find a corridor you can use
corridors = reasy("GET", "/payout-corridors?currency=CNY&transactionMode=B2B&deliveryMethod=bankTransfer")
corridor = corridors[0]
# 2. Get a quote (send 1,000,000 XAF)
quote = reasy("POST", "/rates", {
"externalId": "quote-10042", # unique on your side
"sourceCurrency": "XAF",
"targetCurrency": corridor["targetCurrency"],
"transactionMode": "B2B",
"payoutCorridorId": corridor["id"],
"mode": "SOURCE_AMOUNT",
"sourceAmount": 1000000,
})
print(f"Quote {quote['id']}: {quote['sourceAmount']} XAF -> {quote['destinationAmount']} CNY, "
f"fee {quote['feeAmount']} {quote['feeCurrency']}, expires {quote['expiredAt']}")
# 3. Create the transfer
transfer = reasy("POST", "/transfers", {
"quotationId": quote["id"],
"externalId": "order-10042", # unique on your side
"callbackUrl": "https://example.com/webhooks/reasy",
"purposeOfRemittance": "EXPORTED_GOODS",
"documentReferenceNumber": "INV-10042",
"sendingBusiness": {
"registeredName": "Acme Cameroun SARL",
"registrationNumber": "RC/DLA/2015/B/1234",
"address": "Rue de la Joie, Akwa",
"city": "Douala",
"countryIsoCode": "CMR",
"msisdn": "+237690000000",
},
"receivingBusiness": {
"registeredName": "Pingyao Shanghai Co., Ltd.",
"address": "No. 1888, Century Avenue, Pudong New Area",
"city": "Shanghai",
"countryIsoCode": "CHN",
},
"deliveryMethodInfo": {
"bankAccountHolderFullName": "Pingyao Shanghai Co., Ltd.",
"bankAccountNumber": "6222020200044445555",
"bankAccountSwiftCode": "ICBKCNBJ",
},
})
print(f"Transfer {transfer['id']} is {transfer['status']}")
# 3b. Bank transfers only: upload the supporting invoice before confirming
with open("invoice-10042.pdf", "rb") as f:
session.put(f"{BASE_URL}/transfers/{transfer['id']}/attachment",
data=f, headers={"content-type": "application/pdf"}).raise_for_status()
# 4. Confirm — this submits the payout
reasy("POST", f"/transfers/{transfer['id']}/confirm")
# 5. Track it (you will also receive a webhook when it is paid or failed)
status = reasy("GET", f"/transfers/{transfer['id']}")
print(f"Transfer {status['id']}: {status['status']} ({status['statusCode']})")
Step by step
1. List your payout corridors — GET /payout-corridors
GET /payout-corridorsA payout corridor is a destination you can pay into: a target currency and country, a delivery method and a transaction mode. Only the corridors enabled for your organization are returned.
Optional filters: currency (target currency), destinationCountryIsoCode (ISO 3166-1 alpha-3, e.g. CHN), transactionMode (B2B, B2C, C2B, C2C), deliveryMethod (bankTransfer, alipay, wechat, unionpayCard), limit (1–200, default 100).
[
{
"id": "PAYOUTCORRIDOR#2001",
"sourceCurrencies": ["USD", "EUR", "XAF"],
"targetCurrency": "CNY",
"destinationCountryIsoCode": "CHN",
"transactionMode": "B2B",
"deliveryMethod": "bankTransfer",
"bankName": null,
"isEnabled": true,
"minimumTransactionAmount": 10.0,
"maximumTransactionAmount": 10000.0,
"requiredSendingEntityFields": ["registeredName", "city"],
"requiredReceivingEntityFields": ["registeredName"],
"requiredDeliveryMethodFields": [["bankAccountNumber", "bankAccountSwiftCode", "bankAccountHolderFullName"]]
}
]
idis thepayoutCorridorIdyou pass when requesting a quote.minimumTransactionAmount/maximumTransactionAmountare per transfer, in the target currency.requiredSendingEntityFields/requiredReceivingEntityFieldslist the party fields you must provide onPOST /transfers.requiredDeliveryMethodFieldsis a list of alternatives: yourdeliveryMethodInfomust contain all the fields of at least one group (for example[["msisdn"], ["email"]]means msisdn or email).
2. Get a quote — POST /rates
POST /rates{
"externalId": "quote-10042",
"sourceCurrency": "XAF",
"targetCurrency": "CNY",
"transactionMode": "B2B",
"payoutCorridorId": "PAYOUTCORRIDOR#2001",
"mode": "SOURCE_AMOUNT",
"sourceAmount": 1000000
}
modesays which side is fixed:SOURCE_AMOUNT(you send exactlysourceAmount) orDESTINATION_AMOUNT(the recipient gets exactlydestinationAmount).sourceCurrencyis one ofUSD,EUR,XAF(must be listed in the corridor'ssourceCurrencies).targetCurrencyis the corridor'stargetCurrency.externalIdmust be unique across your quotes (errorCode 10002otherwise).GET /ratesaccepts the same fields as query parameters and also creates a quotation.
{
"id": "2WZF3Q9iKXNzxkE2p9kEJI3knGS",
"externalId": "quote-10042",
"sourceCurrency": "XAF",
"targetCurrency": "CNY",
"payoutCorridorId": "PAYOUTCORRIDOR#2001",
"sourceAmount": 1000000.0,
"destinationAmount": 11655.0,
"feeCurrency": "XAF",
"feeAmount": 7000.0,
"variableFeeAmount": 6000.0,
"fixedFeeAmount": 1000.0,
"transferType": "B2B",
"mode": "SOURCE_AMOUNT",
"createdAt": "2026-08-19T10:00:00Z",
"expiredAt": "2026-08-19T10:15:00Z"
}
- Fees are charged on top of
sourceAmount, infeeCurrency(always the source currency). Your balance is debitedsourceAmount + feeAmount. - The quote is valid until
expiredAt. Validity depends on the corridor, so always readexpiredAtrather than assuming a fixed duration. - Retrieve a quote later with
GET /rates/{id}orGET /rates/ext-{externalId}.
3. Create the transfer — POST /transfers
POST /transfers| Field | Required | Description |
|---|---|---|
quotationId | yes | id of a non-expired quote |
externalId | yes | Your reference. Must be unique across your transfers (errorCode 10011 otherwise). |
callbackUrl | no | HTTPS URL that receives the status webhook. See Webhooks. |
purposeOfRemittance | yes | e.g. EXPORTED_GOODS, SERVICE_CHARGES, SALARY_PAYMENT, FAMILY_SUPPORT, EDUCATION, OTHER (full list in the API reference) |
documentReferenceNumber | no | Invoice or contract number |
sendingBusiness / sender | depends on mode | Who sends: sendingBusiness for B2B/B2C, sender (individual) for C2B/C2C. The sender must be located in CEMAC (CMR, GAB, TCD, COG, GNQ, CAF). |
receivingBusiness / receiver | depends on mode | Who receives: receivingBusiness for B2B/C2B, receiver (individual) for B2C/C2C. Provide at least the corridor's requiredReceivingEntityFields. |
deliveryMethodInfo | yes | Where to pay out. Provide one group of the corridor's requiredDeliveryMethodFields (bank account + SWIFT, msisdn, email, UnionPay cardNumber…). |
The response is the full transfer object (same shape as GET /transfers/{id}):
{
"id": "2WZF419iKXNzxkE2p9kEJI3knGS",
"externalId": "order-10042",
"quotationId": "2WZF3Q9iKXNzxkE2p9kEJI3knGS",
"callbackUrl": "https://example.com/webhooks/reasy",
"status": "created",
"statusCode": "10000",
"createdAt": "2026-08-19T10:01:12Z",
"expiredAt": "2026-08-19T10:15:00Z",
"sendAmount": 1000000.0,
"sourceCurrency": "XAF",
"receiveAmount": 11655.0,
"destinationCurrency": "CNY",
"feeCurrency": "XAF",
"feeAmount": 7000.0,
"variableFeeAmount": 6000.0,
"fixedFeeAmount": 1000.0,
"purposeOfRemittance": "EXPORTED_GOODS",
"documentReferenceNumber": "INV-10042",
"sendingBusiness": { "registeredName": "Acme Cameroun SARL", "...": "..." },
"receivingBusiness": { "registeredName": "Pingyao Shanghai Co., Ltd.", "...": "..." },
"sender": null,
"receiver": null,
"deliveryMethodInfo": { "bankAccountNumber": "6222020200044445555", "bankAccountSwiftCode": "ICBKCNBJ", "...": null }
}
Creating a transfer reserves sendAmount + feeAmount on your balance (it moves from available to pending). The transfer inherits the quote's expiry: confirm it before expiredAt.
4. Upload the supporting document — PUT /transfers/{id}/attachment
PUT /transfers/{id}/attachmentUpload the invoice or contract that justifies the payment before confirming, otherwise confirmation fails with errorCode 10022. Send the raw file as the request body with a Content-Type of application/pdf or image/*. The upload is only accepted while the transfer is in status created.
5. Confirm — POST /transfers/{id}/confirm
POST /transfers/{id}/confirmConfirming submits the payout to our payment network; the transfer moves to submitted. The response is 200 with an empty (null) body. A transfer can only be confirmed once (errorCode 10015) and only before expiredAt (errorCode 10016).
6. Track the transfer
- Webhook (recommended) — when the payout is final we
POSTthe transfer object to yourcallbackUrlwith statuspaidorfailed. See Webhooks. - Poll —
GET /transfers/{id}, orGET /transfers/ext-{externalId}to look up by your own reference. - List —
GET /transfers?startTime=2026-08-12T00:00:00Z&endTime=2026-08-19T00:00:00Z&limit=100. The window is 7 days at most. When a page is full the response carries anx-next-keyheader; send it back as a request header to get the next page.
Transfer statuses
status | statusCode | Meaning | Your balance |
|---|---|---|---|
created | 10000 | Created, waiting for your confirmation | sendAmount + feeAmount reserved (pending) |
submitted | 10001 | Confirmed and submitted to the payout network | reserved |
paid | 20000 | Paid out to the recipient — final | debited |
failed | 100000–100008 | Not paid — final. 100000 cancelled, 100001 reversed after payout (funds returned), 100002 rejected, 100003 invalid beneficiary details, 100004 compliance checks, 100005 limits exceeded, 100006 other provider issue, 100008 insufficient funds | released back to available |
paid and failed are final. statusCode is a string.
Conventions
- Format — JSON request and response bodies,
camelCasefield names,Content-Type: application/json. - Amounts — decimal numbers in major currency units (
1000000= 1,000,000 XAF,12.5= 12.50 EUR). - Timestamps — ISO 8601 in UTC, e.g.
2026-08-19T10:00:00Z. - Identifiers — REasy ids (
id) are opaque strings. Your own references (externalId) must be unique per organization for quotes and for transfers; we recommend fixed-format ids such as UUIDs. - Pagination — list endpoints return up to
limititems and anx-next-keyresponse header when more are available; pass it back as a request header on the next call. - Errors — every error has a JSON body
{"errorCode": <number>, "message": <string | array>}. Branch onerrorCode, not on the HTTP status:400invalid request (10001,messagelists the offending fields),403authentication,500business rule or server error (see the code inerrorCode).
errorCode | Meaning |
|---|---|
10001 | Validation error, or unexpected server error |
10002 | A quote with this externalId already exists |
10003 | Payout corridor not enabled for your organization |
10006 | API key not linked to an organization |
10007 / 10008 | Quote not found (by id / by external id) |
10009 | Quote has expired |
10011 | A transfer with this externalId already exists |
10013 / 10014 | Transfer not found (by external id / by id) |
10015 | Transfer already confirmed |
10016 | Transfer has expired (confirm too late) |
10018 / 10019 | Missing required receiver / delivery-method field for this corridor |
10022 | Supporting document missing (bank transfers) |
10023 | GET /transfers time range exceeds 7 days |
10024 | Insufficient balance |
10027 | Payout submission failed |
