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 and SBP payments: 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¶
- Your server → API
POST /api/v1/paymentswithpayment_method=card_matchingorsbp_matching - API ⇢ Your server
201:status=processing,requisite,holder_name - Your server → PayerRequisites, amount, deadline and a request to attach the receipt
- PayerTransfers and saves the receipt (a PDF or a screenshot from the bank app)
- Your server → API
POST /api/v1/payments/{id}/receiptwith the receipt file - API ⇢ Your server
201: the receipt is accepted,status=pending - APIPasses the receipt to the provider, the provider checks the payment
- API → Your serverCallback:
completedorcanceled
The receipt changes neither the status nor the amount: the result arrives as the usual
callback. Without a receipt the provider cancels the payment on expiry — you
get a canceled callback.
Create a payment¶
POST /api/v1/payments with payment_method: "card_matching" or "sbp_matching". The request
is signed as for SBP, the fields are the same, see
SBP payments → Request fields.
The 201 Created response is the same as for a card transfer (for
sbp_matching — as for SBP):
{
"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¶
Everything as for a card or SBP: 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¶
POST {{base_url}}/api/v1/payments/{id}/receipt uploads the payment receipt. Token scope
payments:create, the signature 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_matchingorsbp_matching; - the payment is not a test one (
is_test: false); - the payment is
processingand its requisites have been issued.
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:
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¶
201 Created — the receipt is accepted and is being passed to the provider:
{
"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 |
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¶
| 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.
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.
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¶
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¶
Payment statuses, callbacks and polling are the same as for SBP: Statuses and Getting the result.
- The provider confirmed the payment by the receipt — a
completedcallback.amountmay differ frominitial_amountif the payment was confirmed for another amount. - No receipt was uploaded, or the payment was not confirmed before expiry — a
canceledcallback.
Sandbox¶
Test payments (is_test: true) with matching methods are created and closed as in the
sandbox: 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¶
- Payment methods — all methods and what the payer gets.
- Card payments and SBP payments — the same payments without a receipt.
- API reference —
uploadPaymentReceiptandgetPaymentReceipt.