Getting Started

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


  1. Discover corridors — GET /payout-corridors lists the destinations enabled for your organization (currency, country, delivery method, limits) and the fields each one requires.
  2. Get a quote — POST /rates returns a quotation with the exchange rate, fees and an expiredAt timestamp. Quotes are short-lived; create the transfer before it expires.
  3. Create the transfer — POST /transfers with the quotationId, your own externalId, the sender/receiver details and the payout details. The send amount plus fees moves from your available to your pending balance.
  4. Confirm — POST /transfers/{id}/confirm submits the payout. Until you confirm, nothing is sent.
  5. Track — we POST the final status to your callbackUrl (paid or failed), and you can always GET /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)

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

A 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"]]
  }
]
  • id is the payoutCorridorId you pass when requesting a quote.
  • minimumTransactionAmount / maximumTransactionAmount are per transfer, in the target currency.
  • requiredSendingEntityFields / requiredReceivingEntityFields list the party fields you must provide on POST /transfers.
  • requiredDeliveryMethodFields is a list of alternatives: your deliveryMethodInfo must contain all the fields of at least one group (for example [["msisdn"], ["email"]] means msisdn or email).

2. Get a quote — POST /rates

{
  "externalId": "quote-10042",
  "sourceCurrency": "XAF",
  "targetCurrency": "CNY",
  "transactionMode": "B2B",
  "payoutCorridorId": "PAYOUTCORRIDOR#2001",
  "mode": "SOURCE_AMOUNT",
  "sourceAmount": 1000000
}
  • mode says which side is fixed: SOURCE_AMOUNT (you send exactly sourceAmount) or DESTINATION_AMOUNT (the recipient gets exactly destinationAmount).
  • sourceCurrency is one of USD, EUR, XAF (must be listed in the corridor's sourceCurrencies). targetCurrency is the corridor's targetCurrency.
  • externalId must be unique across your quotes (errorCode 10002 otherwise).
  • GET /rates accepts 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, in feeCurrency (always the source currency). Your balance is debited sourceAmount + feeAmount.
  • The quote is valid until expiredAt. Validity depends on the corridor, so always read expiredAt rather than assuming a fixed duration.
  • Retrieve a quote later with GET /rates/{id} or GET /rates/ext-{externalId}.

3. Create the transfer — POST /transfers

FieldRequiredDescription
quotationIdyesid of a non-expired quote
externalIdyesYour reference. Must be unique across your transfers (errorCode 10011 otherwise).
callbackUrlnoHTTPS URL that receives the status webhook. See Webhooks.
purposeOfRemittanceyese.g. EXPORTED_GOODS, SERVICE_CHARGES, SALARY_PAYMENT, FAMILY_SUPPORT, EDUCATION, OTHER (full list in the API reference)
documentReferenceNumbernoInvoice or contract number
sendingBusiness / senderdepends on modeWho sends: sendingBusiness for B2B/B2C, sender (individual) for C2B/C2C. The sender must be located in CEMAC (CMR, GAB, TCD, COG, GNQ, CAF).
receivingBusiness / receiverdepends on modeWho receives: receivingBusiness for B2B/C2B, receiver (individual) for B2C/C2C. Provide at least the corridor's requiredReceivingEntityFields.
deliveryMethodInfoyesWhere 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

Upload 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

Confirming 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 POST the transfer object to your callbackUrl with status paid or failed. See Webhooks.
  • Poll — GET /transfers/{id}, or GET /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 an x-next-key header; send it back as a request header to get the next page.

Transfer statuses

statusstatusCodeMeaningYour balance
created10000Created, waiting for your confirmationsendAmount + feeAmount reserved (pending)
submitted10001Confirmed and submitted to the payout networkreserved
paid20000Paid out to the recipient — finaldebited
failed100000–100008Not 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 fundsreleased back to available

paid and failed are final. statusCode is a string.

Conventions

  • Format — JSON request and response bodies, camelCase field 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 limit items and an x-next-key response 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 on errorCode, not on the HTTP status: 400 invalid request (10001, message lists the offending fields), 403 authentication, 500 business rule or server error (see the code in errorCode).
errorCodeMeaning
10001Validation error, or unexpected server error
10002A quote with this externalId already exists
10003Payout corridor not enabled for your organization
10006API key not linked to an organization
10007 / 10008Quote not found (by id / by external id)
10009Quote has expired
10011A transfer with this externalId already exists
10013 / 10014Transfer not found (by external id / by id)
10015Transfer already confirmed
10016Transfer has expired (confirm too late)
10018 / 10019Missing required receiver / delivery-method field for this corridor
10022Supporting document missing (bank transfers)
10023GET /transfers time range exceeds 7 days
10024Insufficient balance
10027Payout submission failed