> ## Documentation Index
> Fetch the complete documentation index at: https://developers.melio.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment attachments

> Uploading a supporting document, the cases where an international destination will not accept a payment without one, and how to recover from 400 ATTACHMENT_REQUIRED.

An attachment is a supporting document — usually an invoice — stored against the business and
referenced from a payment by an opaque id. There are two reasons to use them:

1. Some international destinations will not accept a payment without one.
2. An identity document for a beneficial owner is uploaded the same way (see
   [International onboarding](https://developers.melio.com/docs/international-onboarding)).

## Uploading

```http theme={null}
POST /v2/attachments
api-key: <your key>
Melio-Entity-Id: ent_8c6f...
Content-Type: multipart/form-data
```

Send the document in a part named `file`. PDF, JPEG and PNG are accepted, up to 8 MB.

```json theme={null}
{
  "id": "att_9f8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "fileName": "invoice-2043.pdf",
  "mimeType": "application/pdf",
  "byteSize": 182734,
  "createdAt": "2026-09-07T10:15:00.000Z"
}
```

The bytes are write-only: the response is the id plus enough metadata to recognise which file it was,
and nothing in this API reads a document back. Upload once and reference the id from as many payments
as you need.

| Code | Status | When |
| - | - | - |
| `VALIDATION_ERROR` | 400 | No `file` part, an empty file, more than one file, or an unsupported media type — the message lists the accepted ones |
| `VALIDATION_ERROR` | 400 | The document is over the limit; the message names it in bytes |
| `ATTACHMENT_UPLOAD_FAILED` | 400 | The file service rejected the document |
| `NOT_FOUND` | 404 | The entity in `Melio-Entity-Id` does not exist |

## Referencing one from a payment

```json theme={null}
{
  "amount": 100000,
  "currency": "EUR",
  "originatingAccountId": "acct_3f2504e0-...",
  "receivingAccountId": "acct_7d4e2a10-...",
  "deductionDate": "2026-07-10",
  "deliveryPreference": "international-fx",
  "attachmentId": "att_9f8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "compliance": { "type": "goods-and-services", "purpose": "payment for consulting services" }
}
```

Two constraints:

* **The attachment must belong to the same entity as the payment.** An id from another entity is a
  `404`, not a `403` — the response does not distinguish "not yours" from "does not exist".
* **The document is attached when the payment is created.** `PATCH /payments/{paymentId}` has no
  `attachmentId`: there is nothing to change it to, and re-associating a document on a payment that
  is already moving is not something the rails support. If the wrong document went out, cancel the
  payment and create it again.

Sending an `attachmentId` where nothing requires one is always accepted.

## When a document is required

The requirement is the currency provider's, not ours, and it is evaluated per payment at creation:

| Case | Document required? |
| - | - |
| Domestic rails (ACH, check, wire, virtual card) | No |
| `international-gusd` | No — dollar-denominated, and this check does not apply to it |
| `international-fx`, destination country rated **high** risk | **Always** |
| `international-fx`, destination country rated **medium** risk | **On the first payment to that counterparty.** Once one payment to them exists, later ones do not need it |
| `international-fx`, every other destination | No |

The risk rating belongs to the destination country and is maintained by the provider, so treat the
requirement as data rather than a list you can hard-code: a country can move between ratings without
any change to this API.

Because the medium-risk case keys off "have you paid this counterparty before", the same corridor can
require a document for a new counterparty and not for an established one. Do not cache the answer per
country.

## Handling `400 ATTACHMENT_REQUIRED`

When it applies and no `attachmentId` is given, `POST /payments` fails with:

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "ATTACHMENT_REQUIRED",
    "message": "The destination requires a supporting document. Upload one with POST /attachments and pass its id as attachmentId."
  }
}
```

Nothing is created, so recovery is: upload the document, then retry the same payment with
`attachmentId` set. Use a **new** `Idempotency-Key` for the retry — the original key is bound to the
body you first sent, and reusing it with a different body returns `409 IDEMPOTENCY_KEY_REUSED`.

If you would rather never see this error, the simplest robust integration is to attach the invoice on
every international payment where you have one. It is accepted everywhere, so there is no branch to
get wrong.

## See also

* [International payments](https://developers.melio.com/docs/international-payments) — the two rails and their payloads
* [International onboarding](https://developers.melio.com/docs/international-onboarding) — identity documents for beneficial owners


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.