# Matching: transfer with a receipt

Matching (Russian «матчинг», sometimes spelled «метчинг») is a card or SBP transfer that the
provider credits **only by the payer's receipt**. The payer transfers to the issued requisites,
you upload the receipt file, the provider matches it with the incoming transfer and confirms the
payment. The payment works like [card payments](card.md) and [SBP payments](sbp.md): the same
request, statuses, callbacks and retry rules. What differs is `payment_method` and the receipt
step.

| | Card transfer | Card transfer (matching) | SBP | SBP (matching) |
| --- | --- | --- | --- | --- |
| `payment_method` | `card_transfer` | `card_matching` | `sbp` | `sbp_matching` |
| What the payer gets | a card number | a card number | a phone number | a phone number |
| Receipt after the transfer | not needed | **required** | not needed | **required** |

Matching methods are separate contract methods with their own terms; your manager enables them.
A method missing from the project contract gets `422 payment_not_covered`.

## How the payment goes { #flow }

<div class="pp-seq" data-lanes="Your server|API|Payer" markdown>

1. **Your server → API.** `POST /api/v1/payments` with `payment_method=card_matching` or `sbp_matching`
2. **API ⇢ Your server.** `201`: `status=processing`, `requisite`, `holder_name`
3. **Your server → Payer.** Requisites, amount, deadline and a request to attach the receipt
4. **Payer.** Transfers and saves the receipt (a PDF or a screenshot from the bank app)
5. **Your server → API.** `POST /api/v1/payments/{id}/receipt` with the receipt file
6. **API ⇢ Your server.** `201`: the receipt is accepted, `status=pending`
7. **API.** Passes the receipt to the provider, the provider checks the payment
8. **API → Your server.** Callback: `completed` or `canceled`

</div>

The receipt changes neither the status nor the amount: the result arrives as the usual
[callback](callbacks.md). **Without a receipt the provider cancels the payment on expiry** — you
get a `canceled` callback.

## Create a payment { #create }

`POST /api/v1/payments` with `payment_method: "card_matching"` or `"sbp_matching"`. The request
is [signed](signing.md) as for SBP, the fields are the same, see
[SBP payments → Request fields](sbp.md#request-fields).

```json
{
  "merchant_payment_id": "order-1005",
  "amount": "5000.00",
  "payment_method": "card_matching"
}
```

The `201 Created` response is the same as for a [card transfer](card.md#create) (for
`sbp_matching` — as for [SBP](sbp.md#response)):

```json
{
  "id": "3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42",
  "merchant_payment_id": "order-1005",
  "status": "processing",
  "amount": 5000,
  "initial_amount": 5000,
  "currency": "RUB",
  "geo_code": "RU",
  "payment_method": "card_matching",
  "bank_code": null,
  "is_intrabank": false,
  "requisite": "2200123456789010",
  "requisite_bank": "Сбер",
  "holder_name": "Мария П.",
  "created_at": "2026-10-23T09:30:00Z",
  "updated_at": "2026-10-23T09:30:01Z"
}
```

- `requisite` — a card number (`card_matching`) or a phone number (`sbp_matching`);
- `holder_name`, `requisite_bank` — the recipient and their bank;
- `id` — you need it to upload the receipt.

## What to show the payer { #payer-screen }

Everything as for a [card](card.md#payer-screen) or [SBP](sbp.md#payer-screen): the requisite with
a "Copy" button, the recipient, exactly `amount` and a countdown. In addition:

- **Receipt upload.** After the transfer, ask the payer to attach the receipt — a PDF or a
  screenshot from the bank app showing the amount, date and recipient.
- **A warning.** Without a receipt the payment is not credited and is canceled on expiry.

## Upload the receipt { #upload }

`POST {{base_url}}/api/v1/payments/{id}/receipt` uploads the payment receipt. Token scope
`payments:create`, the [signature](signing.md) is always required.

| | |
| --- | --- |
| Body | `multipart/form-data`, exactly one field — the `receipt` file. No other fields |
| File | JPG, PNG or PDF — the type is checked by content, not by extension |
| Size | file up to 5 MB, body up to 5 MB plus 64 KB — regardless of the project body limit |
| Recommendation | keep the file within **200 KB**: some providers do not accept larger files, and such a receipt does not reach the provider |
| Signature | `Content-Digest` is the SHA-256 of the **exact bytes** of the multipart body as sent |
| How many | one receipt per payment |

The receipt is accepted when:

- the payment method is `card_matching` or `sbp_matching`;
- the payment is not a test one (`is_test: false`);
- the payment is `processing` and its requisites have been issued.

```http
POST {{base_url}}/api/v1/payments/3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42/receipt HTTP/1.1
Authorization: Bearer nl_live_...
Content-Type: multipart/form-data; boundary=receipt-7d1f0c
Content-Digest: sha-256=:…:
X-Request-ID: 6a1e7c3b-2d4f-4b8a-9c0d-1e2f3a4b5c6d
X-Request-Nonce: 2c26b46b68ffc68ff99b453c1d304134
Signature-Input: sig1=(…);created=…;expires=…;keyid="ed25519-…";alg="ed25519"
Signature: sig1=:…:

--receipt-7d1f0c
Content-Disposition: form-data; name="receipt"; filename="receipt.pdf"
Content-Type: application/pdf

<file bytes>
--receipt-7d1f0c--
```

Sign the same bytes you send. The simplest way is to build the multipart body yourself with a
known `boundary`, sign it and send the body as is. A Python example with `sign_request` from the
[ready-made signing functions](signing.md#ready-functions):

```python
import os
import secrets
import urllib.request

from sign import load_private_key, sign_request

payment_id = "3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42"
url = os.environ["BASE_URL"].rstrip("/") + f"/api/v1/payments/{payment_id}/receipt"

with open("receipt.pdf", "rb") as f:
    content = f.read()
boundary = "receipt-" + secrets.token_hex(8)
body = (
    f"--{boundary}\r\n"
    'Content-Disposition: form-data; name="receipt"; filename="receipt.pdf"\r\n'
    "Content-Type: application/pdf\r\n\r\n"
).encode() + content + f"\r\n--{boundary}--\r\n".encode()

with open(os.environ["SIGNING_KEY_FILE"], "rb") as f:
    key = load_private_key(f.read())
headers = sign_request("POST", url, body, key, os.environ["SIGNING_KEY_ID"])
headers["Authorization"] = "Bearer " + os.environ["API_TOKEN"]
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"

request = urllib.request.Request(url, data=body, headers=headers, method="POST")
with urllib.request.urlopen(request, timeout=30) as response:
    print(response.status, response.read().decode())
```

### Response { #upload-response }

`201 Created` — the receipt is accepted and is being passed to the provider:

```json
{
  "payment_id": "3f1c2b9e-6a8d-4c4b-9a71-2c7f5d1e0a42",
  "status": "pending",
  "file_name": "receipt.pdf",
  "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "created_at": "2026-10-23T09:41:00Z"
}
```

| Field | Meaning |
| --- | --- |
| `payment_id` | The payment `id` |
| `status` | Delivery of the receipt to the provider: `pending`, `sent`, `failed` — see [below](#receipt-statuses) |
| `file_name` | The file name; it is cleaned and its extension follows the content |
| `sha256` | SHA-256 of the file, hex |
| `created_at` | When the receipt was accepted, UTC |

**Retrying is safe.** The same file again (same `sha256`) returns `200 OK` with the accepted
receipt. If there was no response (timeout, broken connection), send the same file with **new**
nonce and signature. Another file when a receipt already exists gets
`409 payment_receipt_exists`.

### Errors { #upload-errors }

| HTTP | `error` | When | What to do |
| --- | --- | --- | --- |
| 400 | `invalid_payment_receipt` | The body is not `multipart/form-data`, has no `receipt` file, has another field or repeats one | Fix the request |
| 400 | `attachment_type_not_allowed` | The file is not JPG, PNG or PDF by content | Send a JPG, PNG or PDF |
| 404 | `payment_not_found` | No such payment, it belongs to another project, or `id` is not a UUID | Check the `id` |
| 409 | `payment_receipt_not_allowed` | The receipt is not accepted now; the cause is in `reason` (table below) | Depends on the cause |
| 409 | `payment_receipt_exists` | The payment already has another receipt | No second receipt is needed; check `GET …/receipt` |
| 409 | `attachment_limit_reached` | The payment already has 20 files | Contact support |
| 409 | `request_replayed` | This `X-Request-Nonce` was already used | Retry with a new nonce and signature |
| 413 | `request_too_large` | The body is over 5 MB plus 64 KB, or the file is over 5 MB | Make the file smaller |

Common errors (`401`, `403`, `429`, `500`, `503`) are the same as for other requests, see
[Statuses and errors](statuses.md#http-errors).

```json
{"error": "payment_receipt_not_allowed", "reason": "payment_not_processing"}
```

| `reason` | Why |
| --- | --- |
| `not_matching_method` | The payment method is neither `card_matching` nor `sbp_matching`: no receipt is needed |
| `test_payment` | The payment is a test one — receipts are not accepted for test payments |
| `payment_not_processing` | The payment is already closed (not `processing`) |
| `requisites_not_issued` | Requisites have not been issued yet |

If the payment has already closed as `canceled` but the payer did pay, that is not a matching
receipt but a dispute:
[`POST /api/v1/payments/{id}/disputes`](api-reference.md#createPaymentDispute).

## Receipt status { #receipt-status }

`GET {{base_url}}/api/v1/payments/{id}/receipt` returns the payment receipt and its delivery to
the provider. Scope `payments:read`; the signature only if the project policy requires it
(`signature_required`). The `200` response is the same object as on upload. Errors:
`404 payment_not_found` — no such payment in your project, `404 payment_receipt_not_found` — the
payment has no receipt.

### Receipt statuses { #receipt-statuses }

| `status` | Meaning |
| --- | --- |
| `pending` | The receipt is accepted and is being passed to the provider (retried on network failures) |
| `sent` | The provider accepted the receipt |
| `failed` | The provider did not accept the receipt; staff will resend it |

The receipt status is only about delivering the file. Whether the payment is paid is told by the
payment status and the callback.

## Statuses and result { #result }

Payment statuses, callbacks and polling are the same as for SBP:
[Statuses](sbp.md#statuses) and [Getting the result](sbp.md#result).

- The provider confirmed the payment by the receipt — a `completed` callback. `amount` may
  differ from `initial_amount` if the payment was confirmed for another amount.
- No receipt was uploaded, or the payment was not confirmed before expiry — a `canceled`
  callback.

## Sandbox { #sandbox }

Test payments (`is_test: true`) with matching methods are created and closed as in the
[sandbox](sandbox.md#outcome): the amount sets the outcome. A test payment does not accept a
receipt — `409 payment_receipt_not_allowed` with `reason: "test_payment"`. This lets you check how
you handle a receipt upload error.

## See also { #see-also }

- [Payment methods](methods.md) — all methods and what the payer gets.
- [Card payments](card.md) and [SBP payments](sbp.md) — the same payments without a receipt.
- [API reference](api-reference.md#uploadPaymentReceipt) — `uploadPaymentReceipt` and
  `getPaymentReceipt`.