Skip to main content

Receipts and review

How customers send their bank receipt, how your team approves it fully or partly or rejects it in one queue, and how a receipt and a bank SMS never count one transfer twice.

A receipt is the customer's evidence for one payment: the photo, screenshot or PDF their bank app gave them, with what they typed beside it. Bezanbecard is receipt first. The customer pays from their own bank to your card or account, uploads the receipt on the checkout, and your team decides in one queue. Nothing is approved until a member of your team approves it, unless you turn automatic approval on. Reviewers are always your own team, never Bezanbecard staff.

Sending a receipt

On the hosted checkout, after paying, the customer adds up to three files and presses "Send receipt". They may also give the tracking number on the receipt, the amount they sent and when they paid, and on the bank transfer tab they say which kind of transfer they made.

  • Files: JPEG, PNG, WebP, HEIC or PDF, at most 10 MB each, recognised by their bytes. They are stored exactly as uploaded, with a SHA-256 fingerprint of each (see the evidence pack).
  • A file is required by default. Turn that off in your checkout settings and a customer may report the transfer without one; it is still a receipt, with no file.
  • The payment becomes Awaiting approvalprocessing and waits for your team. It never goes to review by a timer: the receipt queue is the review.
  • More than one: a customer who paid in two transfers sends two receipts. A corrected receipt (a clearer photo) replaces their earlier pending one instead.
  • Another way: when a customer sent the receipt on a messenger or handed it over in person, a teammate adds it from the payment's page in the dashboard, or your server sends it with POST /v1/payments/:id/receipts.

The customer sees each of their receipts on the checkout page with its status, and the reason when one is rejected. Bezanbecard never messages the customer about a decision. Your team hears about each new receipt by Telegram and email (the receipt notification, on by default for owners, admins and support), and your server by the receipt.created webhook.

The review queue

Receipts in the dashboard lists every receipt, pending first, and follows new ones live. Each one sits beside its payment: the amount the payment expects, the files, what the customer typed, a warning when it looks like a duplicate, and a note when the bank SMS matched. Filter by status and transfer kind, or search by tracking number, payer name or id.

The review queue
StatusMeaning
Pending reviewpendingWaiting for your team.
ApprovedapprovedA member (or automatic approval) accepted it, and its money is on the payment.
RejectedrejectedA member turned it down with a reason the customer sees.
ReplacedwithdrawnA corrected receipt from the same customer replaced it.

Owners, admins and support members can approve, reject and reopen receipts. Developers and viewers can look but not decide.

Approving

Approving says "this money reached us". The approved amount is the amount on the receipt when the customer gave one, otherwise the amount the payment asked for when the receipt was sent (so its unique digits stay even after the checkout expired), otherwise the amount the payment expects now, otherwise what remains. A copy of another pending receipt that states no amount starts at the amount of the bank SMS that matched its original, so approving the copy first uses that SMS. Approving exactly the amount the payment expects pays it in full: its unique digits never count as extra or as missing. Change it before you confirm when your bank shows another figure.

  • The full amount: the payment becomes Succeededsucceeded.
  • Part of the amount: approve what really arrived. The payment becomes Partially paidpartially_paid with a fresh unique amount for the rest, and the checkout asks the customer for it. When partial payments are off, or the checkout's deadline has passed, it waits in Needs reviewrequires_review instead, until your team asks for the rest or accepts the shortfall; the approval dialog says which. On a payment your team closed with a shortfall, a part approved keeps it Succeededsucceeded, and what is still missing stays its accepted shortfall. See partial payments.
  • More than asked: approving more than the payment still asks for records the extra as an overpayment, so it is refused with receipt_payment_settled until you confirm the overpayment (overpay: true). The dashboard names that figure before you confirm. Linking a bank SMS to a receipt from Review and fix asks for the same confirm when it overpays.
  • A payment with nothing left to pay: all the money an approval adds is an overpayment, so the same confirm applies (overpay: true). Send amount with it when the receipt states none; the payment's old expected amount is never booked again (receipt_amount_required). A payment your team closed with a shortfall still takes the missing money without that confirm.
  • A canceled or expired payment: approving reopens it, so the dashboard asks you to confirm that first. Its deposits then decide its status again, and only money beyond what it asked for stays owed back to the customer. Once its money was given back (refunded or credited), no receipt is approved on it: receipt_payment_refunded. A new transfer needs a new payment. If a late transfer reopens it anyway, the money given back still pays nothing, and a receipt sent after that decision never stands on that money or is marked by its SMS: approving it adds its own transfer.
  • A payment whose overpayment you already resolved: approving money that would overpay it beyond what you resolved is refused with receipt_overpayment_resolved.
  • A receipt the bank SMS matched that states the SMS's amount is approved at that amount only; any other amount is receipt_amount_mismatch. If the bank and the receipt disagree, detach the SMS deposit first. A receipt that states no amount starts at the SMS's amount; if it is another transfer, approve it at its own amount, and the SMS stays on the payment for its own receipt.

An approval is backed by exactly one deposit on the payment: the bank SMS deposit that matched it, an unmatched deposit you pick, or a new deposit of source receipt. When the deposit of that amount backs another approved receipt, that receipt may move to another deposit of its own amount on the payment that fits it, so both stand on their own transfer: no money moves, and the moved receipt's history says so. A double click or a retried request approves once. When two teammates decide the same receipt at once, one wins and the other gets receipt_action_unavailable: the receipt is already decided. In the dashboard, receipt_already_changed means what the approval dialog showed no longer holds: it said the approval adds no money or reopens nothing, or it named an overpayment, and a teammate's decision, a detach or a cancel changed that meanwhile. The dialog then shows what approving does now. Changes to one payment's money run one at a time: when another change to the same payment is still running, your request waits a few seconds, then answers payment_already_changed with nothing done, and a retry with the same Idempotency-Key runs.

curl
curl https://a.bezanbecard.com/v1/receipts/rcp_…/approve \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: <a new UUID per decision>" \
  -H "Content-Type: application/json" \
  -d '{"amount":10000000,"note":"Bank app shows 10,000,000"}'

Make a new Idempotency-Key for each decision and reuse it only to retry that same request. A receipt that went back to pending (reopened, or its deposit detached) needs a new key for its next decision; the old one would only replay the first answer.

Rejecting

Reject with one of these reasons. The customer sees the reason on the checkout page; your note stays with your team.

Rejecting
reasonThe customer reads
not_receivedNo matching transfer reached the account.
amount_mismatchThe receipt's amount does not match the order.
unreadableThe receipt cannot be read.
duplicateThis receipt was sent before.
otherThe receipt was not accepted.

Rejecting never moves money. When the payment has no other pending receipt, it waits for the customer again: Awaiting transferrequires_payment, or Partially paidpartially_paid if part had arrived. The customer can then send another receipt before the checkout closes.

curl
curl https://a.bezanbecard.com/v1/receipts/rcp_…/reject \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: <a new UUID per decision>" \
  -H "Content-Type: application/json" \
  -d '{"reason":"unreadable","note":"Blurred photo"}'

Changed your mind? Press Back to review on a rejected receipt's page in the dashboard: it goes back to pending and the payment back to Awaiting approvalprocessing. An approved receipt reopens by detaching its deposit, which puts it back to pending by itself (see corrections).

Duplicate warnings

When a receipt arrives, the fingerprint of each file and its tracking number are compared with every other receipt of your business in the same mode. A match is shown to the reviewer as a warning, with a link to the other receipt; it is never a refusal, because only your team knows whether it is a resend or the same receipt reused for another order. In the API the receipt says duplicate: true.

The bank SMS and automatic approval (optional)

None of this is needed. With the SMS app connected, the deposit SMS your bank sends lands beside the receipt it belongs to:

  • A bank SMS matched to the payment, on its card or account, corroborates the oldest pending receipt it fits: the amount the receipt states, and its paying card, if both are known. A receipt sent with no amount typed claims the amount the payment asked for at that moment, the figure the checkout showed, so the SMS of an earlier part never takes over a receipt sent for the rest. A copy of another receipt claims no amount of its own. One SMS marks one receipt. A second copy of the receipt, or a second instalment of the same amount and card, is not marked by it and waits for its own SMS. The queue says the bank SMS matched, and approving uses that deposit with one click.
  • A receipt added after its SMS arrived is marked the same way, when no other receipt holds that SMS.
  • Approving a receipt on a bank SMS deposit you pick, or linking the SMS to it from the Review and fix inbox, also marks it corroborated, when the SMS fits the amount and paying card the receipt states. Attaching a bank SMS to the payment by hand marks the oldest pending receipt it fits right away.
  • If you approve the copy or the second receipt first at the SMS's amount, and the SMS fits it too, the approval uses that SMS and adds no money: the mark moves to it, so one transfer is never counted twice. A copy that states no amount starts at that amount. The other receipt then adds its own money when you approve it, or you reject it as the duplicate. An SMS that an approved receipt stands on, or that marks another receipt and does not fit this one, cannot be used for it. An SMS from another card than the receipt names is used for it only once the payment is fully paid; while money is still owed it is the other part of a split transfer, and the approval adds its own money, which the receipt's own SMS later replaces.
  • When you reject a marked receipt, or the customer sends a corrected one in its place, the mark passes to the oldest other pending receipt the SMS fits.
  • A deposit you record by hand, or one recorded through the API, never marks a receipt as matched by the bank SMS. Approving a receipt at that deposit's amount still stands on it, with no second deposit.
  • Automatic approval is a setting in your payment settings, off by default. When it is on, the payment has a single pending receipt, the SMS fits it, and the payment is neither waiting in review for another reason nor canceled or expired, the receipt is approved by the system. A late transfer reopens a canceled or expired payment first, and the receipt it fits is then approved; with no new money, a canceled or expired payment stays closed and its receipt waits for a person. A receipt flagged as a copy of another is only marked, and a person decides. With several pending receipts it only marks the oldest one it fits, and a person decides.

Without the app, nothing changes for the customer: you check your bank app and approve.

One transfer, counted once

An approval made before the bank SMS arrives stands on a provisional deposit of source receipt. When the SMS of the same transfer arrives later (the same card or account and the same amount, not from another paying card than the receipt names, with exactly one such receipt deposit, on a payment still open or paid within the last 7 days), the SMS deposit takes its place:

  1. The SMS deposit is attached to the payment.
  2. The receipt deposit becomes Reversedreversed, replaced by the SMS. In the API the SMS deposit, and its deposit.matched event, name it in replaces (a dep_… id, otherwise null): money you already booked for that receipt deposit is this same transfer, never new money.
  3. The receipt now stands on the SMS deposit, which also marks it corroborated when the SMS fits the amount and paying card the receipt states.

amount_received stays the same when the amounts agree. When the engine finds two possible receipt deposits, or none, or a pending receipt of another payment states that same amount, the SMS waits in Review and fix with a suggestion naming the receipts, and a member links it by hand. Attaching it by hand to the payment that holds the receipt deposit replaces that deposit the same way; when two or more receipt deposits there could be this transfer, your attach replaces the oldest. Linking it to the receipt that stands on a receipt deposit replaces that deposit only with an SMS of the same amount, since a replacement never moves money; another amount is receipt_amount_mismatch. Detach the receipt deposit first, then link the SMS to the receipt, which is pending again. Throughout:

  • every approved receipt is backed by exactly one matched deposit;
  • one transfer never counts twice, and amount_received is always the sum of the matched deposits;
  • one bank SMS marks at most one receipt;
  • rejecting never moves money;
  • detaching a receipt's deposit returns the receipt to pending.

Unique amounts on or off

By default every open payment on a card or account asks for its own expected_amount, a few rial apart. Receipts are then easy to check against your bank statement, and a bank SMS matches exactly one payment.

You can turn unique amounts off in your payment settings. expected_amount then equals amount, the checkout says nothing about digits to type, and several open payments may ask for the same figure. Receipts work exactly as before. A bank SMS, however, can no longer tell such payments apart: it never guesses, and waits in Review and fix for a person. Keep unique amounts on if you rely on the SMS app.

Receipts in the API

Receipts in the API
FieldDescription
idrcp_…
paymentThe payment it is evidence for.
statuspending, approved, rejected or withdrawn.
channelcard, pol, paya, satna, internal, or null when the customer did not say.
referenceThe tracking number the customer typed, in Latin letters and digits.
amountWhat the customer says they sent; null means the expected amount.
paid_at, payer_name, payer_last4What the customer said about the payment.
files[{ id, size, sha256 }]: never a URL. The API never serves the files back.
duplicate, corroboratedThe two warnings: another receipt with the same file or tracking number, and a bank SMS of the same transfer.
review{ decision, amount, reason, reviewed_at } once decided.
createdUnix seconds.
Receipts in the API
RequestDoes
GET /v1/receiptsLists receipts, newest first, filtered by status (pending is the queue), channel, payment and created.
GET /v1/receipts/:idRetrieves one.
POST /v1/payments/:id/receiptsSubmits a receipt: JSON, or multipart/form-data with the files in files. The fields are those of the checkout with paid_at in unix seconds.
POST /v1/receipts/:id/approveApproves, with optional amount, deposit (an unmatched dep_… to stand on), overpay and note.
POST /v1/receipts/:id/rejectRejects, with a reason and an optional note.

A payment carries its receipts with expand[]=receipts. A restricted key needs receipts access, and submitting needs payments and receipts set to write. The events are receipt.created, receipt.approved and receipt.rejected; they carry the receipt object, never a way to read its files. The API does not say which deposit backs a receipt: read the payment's deposits.

curl
curl https://a.bezanbecard.com/v1/payments/pay_…/receipts \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: receipt-1042-1" \
  -F channel=paya \
  -F reference=140508041234 \
  -F amount=12500470 \
  -F [email protected]

Trying it in test mode

  1. Create a test payment and open its checkout_url.
  2. Upload any image as the receipt and press "Send receipt".
  3. Approve it in Receipts. The payment succeeds on a deposit of source receipt.
  4. Play the bank SMS of the same transfer with POST /v1/test_helpers/deposits and {"payment":"pay_…","amount":<the expected amount>}. The SMS deposit replaces the receipt deposit, the receipt shows that the bank matched, and amount_received does not change.