Skip to content

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

  1. Your server → APIPOST /api/v1/payments with payment_method=card_matching or sbp_matching
  2. API ⇢ Your server201: status=processing, requisite, holder_name
  3. Your server → PayerRequisites, amount, deadline and a request to attach the receipt
  4. PayerTransfers and saves the receipt (a PDF or a screenshot from the bank app)
  5. Your server → APIPOST /api/v1/payments/{id}/receipt with the receipt file
  6. API ⇢ Your server201: the receipt is accepted, status=pending
  7. APIPasses the receipt to the provider, the provider checks the payment
  8. API → Your serverCallback: completed or canceled

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.

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

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_matching or sbp_matching;
  • the payment is not a test one (is_test: false);
  • the payment is processing and 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.

{"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.

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 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

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