Upload the supporting document of a transfer — the invoice, contract or other document that justifies the payment.
An attachment is required on every transfer. POST /transfers/{id}/confirm refuses to submit a transfer that has no attachment (errorCode 10022), so upload it after creating the transfer and before confirming it.
Where it fits
POST /transfers— create the transfer. It iscreatedand nothing has been sent yet.PUT /transfers/{id}/attachment— upload the supporting document.POST /transfers/{id}/confirm— submit the payout.
All three steps must happen before the transfer's expiredAt.
Request
PUT /public/transfers/2WZF419iKXNzxkE2p9kEJI3knGS/attachment HTTP/1.1
Host: api.reasy.fr
x-api-key: YOUR_API_KEY
Content-Type: application/pdf
<the raw bytes of the file>
| Part | Required | Description |
|---|---|---|
transfer_id (path) | yes | The REasy id returned by POST /transfers. The ext-{externalId} form is not supported on this endpoint. |
x-api-key (header) | yes | Your organization's API key. |
Content-Type (header) | yes | The type of the file: application/pdf, image/jpeg or image/png. It must match the file you send — it tells our payout network how to read the document. |
| Body | yes | The file itself, as raw bytes. Not JSON, not base64, not multipart/form-data. Maximum size: 10 MB. |
Rules:
- One attachment per transfer. Uploading again replaces the previous file, so you can correct a wrong document.
- Only while the transfer is
created. Once the transfer is confirmed the attachment can no longer be added or replaced (errorCode 11002). - If you have several documents for one payment, merge them into a single PDF.
Response
200 OK with an empty body. The attachment is stored as soon as you receive the 200; you can confirm the transfer right away.
Errors
Errors on this endpoint are returned with HTTP 403 and the usual JSON body:
{ "message": "Transfer has already been submitted to payout partner.", "errorCode": 11002 }
| HTTP | errorCode | Meaning | What to do |
|---|---|---|---|
403 | 11004 | No transfer with this id exists for your organization | Use the REasy id from POST /transfers, not your externalId |
403 | 11002 | The transfer is no longer created (already confirmed, paid or failed) | Nothing to upload; attachments are frozen once the transfer is confirmed |
403 | 10006 | The API key is not linked to an organization | Contact REasy |
403 | 11003 | Temporary error while authorizing the upload | Retry; contact REasy if it persists |
401 / 403 | — | Missing or invalid x-api-key | Check the header |
413 | — | The file is larger than 10 MB | Compress the file or reduce the scan resolution |
5xx | — | The file could not be stored | Retry the upload — it is safe to repeat |
Confirming without an attachment
If you call POST /transfers/{id}/confirm before uploading, the confirmation is refused:
{ "errorCode": 10022, "message": "The required invoice file for transaction 2WZF419iKXNzxkE2p9kEJI3knGS was not found." }
This refusal is recoverable. Nothing has been sent and the transfer is not failed: it stays created, and sendAmount + feeAmount stays reserved on your balance. Upload the attachment, then call confirm again — before expiredAt.
Examples
cURL
curl -X PUT "https://<base_url>/transfers/$TRANSFER_ID/attachment" \
-H "x-api-key: $REASY_API_KEY" \
-H "Content-Type: application/pdf" \
--data-binary "@invoice-10042.pdf"
Use --data-binary (not -d, which alters the file) and always set Content-Type yourself.
JavaScript (Node.js 18+)
import { readFile } from "node:fs/promises";
const BASE_URL = "https://<base_url>";
const API_KEY = process.env.REASY_API_KEY;
async function uploadAttachment(transferId, filePath, contentType) {
const res = await fetch(`${BASE_URL}/transfers/${transferId}/attachment`, {
method: "PUT",
headers: { "x-api-key": API_KEY, "content-type": contentType },
body: await readFile(filePath),
});
if (!res.ok) {
throw new Error(`REasy attachment upload failed: ${res.status} ${await res.text()}`);
}
}
await uploadAttachment(transfer.id, "invoice-10042.pdf", "application/pdf");
await reasy("POST", `/transfers/${transfer.id}/confirm`); // see Getting Started for reasy()
PHP (8.1+)
<?php
const BASE_URL = 'https://<base_url>';
$apiKey = getenv('REASY_API_KEY');
function uploadAttachment(string $transferId, string $filePath, string $contentType): void
{
global $apiKey;
$ch = curl_init(BASE_URL . "/transfers/$transferId/attachment");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["x-api-key: $apiKey", "content-type: $contentType"],
CURLOPT_POSTFIELDS => file_get_contents($filePath),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("REasy attachment upload failed: $status $raw");
}
}
uploadAttachment($transfer['id'], 'invoice-10042.pdf', 'application/pdf');
reasy('POST', "/transfers/{$transfer['id']}/confirm"); // see Getting Started for reasy()
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 upload_attachment(transfer_id: str, file_path: str, content_type: str) -> None:
with open(file_path, "rb") as f:
res = session.put(
f"{BASE_URL}/transfers/{transfer_id}/attachment",
data=f,
headers={"content-type": content_type},
)
if res.status_code != 200:
raise RuntimeError(f"REasy attachment upload failed: {res.status_code} {res.text}")
upload_attachment(transfer["id"], "invoice-10042.pdf", "application/pdf")
reasy("POST", f"/transfers/{transfer['id']}/confirm") # see Getting Started for reasy()
Common mistakes
- Wrapping the file in JSON, base64 or a multipart form. The body must be the file's bytes and nothing else.
- A missing or generic
Content-Type(application/octet-stream, or theapplication/x-www-form-urlencodedthat cURL sends by default). Onlyapplication/pdfandimage/*files can be read by our payout network; a file sent with another type can make the payout fail. - Using your
externalIdin the path. This endpoint only accepts the REasyid. - Confirming first. Confirm returns
10022until the attachment is there — and once a transfer is confirmed, the attachment can no longer be changed.
