# Матчинг: перевод с чеком

Матчинг (иногда пишут «метчинг», англ. *matching*) — перевод на карту или по СБП, который
провайдер зачисляет **только по чеку плательщика**. Плательщик переводит на выданные
реквизиты, вы присылаете файл чека, провайдер сверяет его с поступлением и подтверждает
оплату. Заявка устроена так же, как [приём по карте](card.md) и [приём по СБП](sbp.md): тот же
запрос, те же статусы, callback-и и правила повторов. Отличаются `payment_method` и шаг с чеком.

| | Перевод на карту | Перевод на карту (матчинг) | СБП | СБП (матчинг) |
| --- | --- | --- | --- | --- |
| `payment_method` | `card_transfer` | `card_matching` | `sbp` | `sbp_matching` |
| Что получает плательщик | номер карты | номер карты | номер телефона | номер телефона |
| Чек после перевода | не нужен | **нужен** | не нужен | **нужен** |

Методы матчинга — отдельные методы договора: у них свои условия, подключает их менеджер.
Метод, которого нет в договоре проекта, — `422 payment_not_covered`.

## Как проходит оплата { #flow }

<div class="pp-seq" data-lanes="Ваш сервер|API|Плательщик" markdown>

1. **Ваш сервер → API.** `POST /api/v1/payments` с `payment_method=card_matching` или `sbp_matching`
2. **API ⇢ Ваш сервер.** `201`: `status=processing`, `requisite`, `holder_name`
3. **Ваш сервер → Плательщик.** Реквизиты, сумма, срок и просьба приложить чек
4. **Плательщик.** Переводит и сохраняет чек (PDF или скриншот из приложения банка)
5. **Ваш сервер → API.** `POST /api/v1/payments/{id}/receipt` с файлом чека
6. **API ⇢ Ваш сервер.** `201`: чек принят, `status=pending`
7. **API.** Передаёт чек провайдеру, провайдер сверяет оплату
8. **API → Ваш сервер.** Callback: `completed` или `canceled`

</div>

Чек не меняет ни статус, ни сумму заявки: итог приходит обычным [callback-ом](callbacks.md).
**Без чека провайдер отменит заявку по сроку** — придёт callback `canceled`.

## Создать заявку { #create }

`POST /api/v1/payments` с `payment_method: "card_matching"` или `"sbp_matching"`. Запрос
[подписывается](signing.md) так же, как для СБП, поля — те же, см.
[Приём по СБП → Поля запроса](sbp.md#request-fields).

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

Ответ `201 Created` — такой же, как у [перевода на карту](card.md#create) (у `sbp_matching` —
как у [СБП](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` — номер карты (`card_matching`) или номер телефона (`sbp_matching`);
- `holder_name`, `requisite_bank` — получатель и его банк;
- `id` — понадобится для загрузки чека.

## Что показать плательщику { #payer-screen }

Всё, что для [карты](card.md#payer-screen) или [СБП](sbp.md#payer-screen): реквизит с кнопкой
«Скопировать», получатель, ровно `amount` и обратный отсчёт срока. Дополнительно:

- **Загрузка чека.** После перевода попросите плательщика приложить чек — PDF или скриншот
  из приложения банка, где видны сумма, дата и получатель.
- **Предупреждение.** Без чека оплата не будет зачислена, а заявка отменится по сроку.

## Прислать чек { #upload }

`POST {{base_url}}/api/v1/payments/{id}/receipt` — загрузка чека заявки. Право токена
`payments:create`, [подпись](signing.md) обязательна всегда.

| | |
| --- | --- |
| Тело | `multipart/form-data`, ровно одно поле — файл `receipt`. Других полей нет |
| Файл | JPG, PNG или PDF — тип проверяется по содержимому, а не по расширению |
| Размер | файл до 5 МБ, тело до 5 МБ плюс 64 КБ — независимо от лимита тела проекта |
| Рекомендация | файл не больше **200 КБ**: часть провайдеров больший файл не принимает, и такой чек до провайдера не дойдёт |
| Подпись | `Content-Digest` — SHA-256 **точных байтов** multipart-тела, как оно уходит в запрос |
| Сколько чеков | один на заявку |

Когда чек принимается:

- метод заявки — `card_matching` или `sbp_matching`;
- заявка не тестовая (`is_test: false`);
- заявка в статусе `processing`, и реквизиты уже выданы.

```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="чек.pdf"
Content-Type: application/pdf

<байты файла>
--receipt-7d1f0c--
```

Подписывайте те же байты, что отправляете. Удобнее всего собрать multipart-тело самому, с
известным `boundary`, посчитать по нему подпись и отправить тело как есть. Пример на Python с
функцией `sign_request` из [готовых функций подписи](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())
```

### Ответ { #upload-response }

`201 Created` — чек принят и передаётся провайдеру:

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

| Поле | Что значит |
| --- | --- |
| `payment_id` | `id` заявки |
| `status` | доставка чека провайдеру: `pending`, `sent`, `failed` — см. [ниже](#receipt-statuses) |
| `file_name` | имя файла; оно очищается, расширение приводится к содержимому |
| `sha256` | SHA-256 файла, hex |
| `created_at` | когда чек принят, UTC |

**Повтор безопасен.** Тот же файл ещё раз (тот же `sha256`) — `200 OK` с уже принятым чеком.
Если ответа не было (таймаут, обрыв), отправьте тот же файл с **новыми** nonce и подписью.
Другой файл, когда чек уже есть, — `409 payment_receipt_exists`.

### Ошибки { #upload-errors }

| HTTP | `error` | Когда | Что делать |
| --- | --- | --- | --- |
| 400 | `invalid_payment_receipt` | Тело не `multipart/form-data`, нет файла `receipt`, есть другое поле или поле повторяется | Исправить запрос |
| 400 | `attachment_type_not_allowed` | Файл по содержимому не JPG, PNG и не PDF | Прислать JPG, PNG или PDF |
| 404 | `payment_not_found` | Заявки нет, она другого проекта или `id` не UUID | Проверить `id` |
| 409 | `payment_receipt_not_allowed` | Чек сейчас не принимается, причина — в `reason` (таблица ниже) | По причине |
| 409 | `payment_receipt_exists` | У заявки уже есть другой чек | Второй чек не нужен; статус — `GET …/receipt` |
| 409 | `attachment_limit_reached` | У заявки уже 20 файлов | Связаться с поддержкой |
| 409 | `request_replayed` | Этот `X-Request-Nonce` уже был | Повторить с новым nonce и подписью |
| 413 | `request_too_large` | Тело больше 5 МБ плюс 64 КБ или файл больше 5 МБ | Уменьшить файл |

Общие ошибки (`401`, `403`, `429`, `500`, `503`) — как у остальных запросов, см.
[Статусы и ошибки](statuses.md#http-errors).

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

| `reason` | Почему |
| --- | --- |
| `not_matching_method` | Метод заявки — не `card_matching` и не `sbp_matching`: чек не нужен |
| `test_payment` | Заявка тестовая — чеки к тестовым заявкам не принимаются |
| `payment_not_processing` | Заявка уже закрыта (не `processing`) |
| `requisites_not_issued` | Реквизиты ещё не выданы |

Если заявка уже закрылась `canceled`, а плательщик оплатил, — это не чек матчинга, а
апелляция: [`POST /api/v1/payments/{id}/disputes`](api-reference.md#createPaymentDispute).

## Статус чека { #receipt-status }

`GET {{base_url}}/api/v1/payments/{id}/receipt` — чек заявки и его доставка провайдеру. Право
`payments:read`; подпись — только если её требует политика проекта (`signature_required`).
Ответ `200` — тот же объект, что при загрузке. Ошибки: `404 payment_not_found` — такой
заявки в проекте нет, `404 payment_receipt_not_found` — у заявки нет чека.

### Статусы чека { #receipt-statuses }

| `status` | Что значит |
| --- | --- |
| `pending` | Чек принят, передаём провайдеру (при сбое связи — с повторами) |
| `sent` | Провайдер принял чек |
| `failed` | Провайдер не принял чек; сотрудники отправят его ещё раз |

Статус чека — это только доставка файла. Оплачена ли заявка, говорит статус заявки и
callback.

## Статусы и результат { #result }

Статусы заявки, callback-и и опрос — такие же, как у СБП:
[Статусы](sbp.md#statuses) и [Как узнать результат](sbp.md#result).

- Провайдер подтвердил оплату по чеку — callback `completed`. Сумма в `amount` может
  отличаться от `initial_amount`, если оплату подтвердили на другую сумму.
- Чек не прислали или оплата не подтвердилась до срока — callback `canceled`.

## Песочница { #sandbox }

Тестовые заявки (`is_test: true`) с методами матчинга создаются и закрываются, как в
[песочнице](sandbox.md#outcome): исход задаёт сумма. Чек к тестовой заявке не принимается —
`409 payment_receipt_not_allowed` с `reason: "test_payment"`. Так можно проверить разбор ошибки
загрузки чека.

## Смотрите также { #see-also }

- [Методы оплаты](methods.md) — все методы и что получает плательщик.
- [Приём по карте](card.md) и [Приём по СБП](sbp.md) — те же заявки без чека.
- [Справочник API](api-reference.md#uploadPaymentReceipt) — `uploadPaymentReceipt` и
  `getPaymentReceipt`.