Reconcile bank-transfer receipts
Check that each bank-transfer receipt has the amount, your account's IBAN and the registration reference, one by one or in batches.
If you collect registrations, fees or licences by bank transfer, you receive receipts as PDFs or phone screenshots and
someone has to match them with each pending payment. With the payment_receipt type Constaia extracts the receipt
data and compares it with what you expect.
Options
{
"expect": "payment_receipt",
"checks": {
"expected_amount": 45,
"expected_iban": "ES7921000813610123456789",
"expected_reference": "INSCRIPCION 123"
},
"metadata": { "registration_id": "123" }
}| Check | What it compares | If it doesn't match |
|---|---|---|
expected_amount | The amount field with the expected amount (half-cent tolerance). | expected_amount with error. |
expected_iban | Your IBAN with the IBANs on the receipt. Case and spaces are ignored. | expected_iban with error. |
expected_reference | Your code inside reference or concept. Ignores case and accents; it just has to be contained. | expected_reference with error. |
Also, every IBAN on the receipt goes through the deterministic iban_checksum validation (check digits). If one fails,
the verdict is invalid with an iban_checksum reason.
expected_iban doesn't distinguish payer and beneficiary
expected_iban accepts the receipt if your IBAN appears as beneficiary_iban or as payer_iban. To make sure the
money goes to your account, also compare fields.beneficiary_iban.value in your code, as in the example below.
Other useful options:
checks.max_age_days: on the transfer date (date). For example30to reject old, reused receipts.checks.holder.full_name: compares withpayer_name, if the registrant must pay themselves.
Fields you get
| Field | Example |
|---|---|
amount, currency | 45, EUR |
date | 2026-09-20 |
payer_name, payer_iban | MARÍA GARCÍA LÓPEZ, ES9121000418450200051332 |
beneficiary_name, beneficiary_iban | CLUB DEPORTIVO ARCO MADRID, ES7921000813610123456789 |
concept, reference | INSCRIPCION 123 |
One receipt
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia(); // reads CONSTAIA_API_KEY
const OUR_IBAN = process.env.OUR_IBAN; // ES7921000813610123456789
export async function reconcile(path, payment) {
const analysis = await constaia.analyze(await fromPath(path), {
expect: "payment_receipt",
checks: {
expectedAmount: payment.amount,
expectedIban: OUR_IBAN,
expectedReference: payment.reference,
},
language: "en",
metadata: { registration_id: String(payment.registrationId) },
});
const beneficiary = analysis.fields.beneficiary_iban?.value?.replace(/\s+/g, "").toUpperCase();
const failed = (analysis.verdict?.reasons ?? []).filter((r) => r.severity !== "info");
if (beneficiary && beneficiary !== OUR_IBAN) {
failed.push({ code: "beneficiary_iban", severity: "error", message: "The beneficiary is not our account." });
}
return {
analysisId: analysis.id,
status: failed.some((r) => r.severity === "error") ? "invalid" : (analysis.verdict?.status ?? "review"),
failedCodes: failed.map((r) => r.code),
messages: failed.map((r) => r.message),
paidAt: analysis.fields.date?.value ?? null,
payerIban: analysis.fields.payer_iban?.value ?? null,
};
}
try {
console.log(
await reconcile("./payment_receipt.pdf", { registrationId: 123, amount: 45, reference: "INSCRIPCION 123" }),
);
} catch (err) {
if (err instanceof ConstaiaError) console.error(err.code, err.message, err.requestId);
else throw err;
}What failed: reading the reason
With invalid, the code of each reason with severity: "error" tells you what doesn't match, and the message
includes the value found and the expected one. For example, if you expected 50 and the receipt says 45 (with
"language": "en"):
{ "code": "expected_amount", "severity": "error", "message": "amount is “45”, expected “50”." }code | Usual action |
|---|---|
expected_amount | Partial payment or wrong amount: ask for the difference or check the fee applied. |
expected_iban | The transfer isn't to your account: reject. |
expected_reference | The registration code is missing: look for the payment by amount and payer, or ask. |
iban_checksum | An IBAN fails its check digits: misread or tampered receipt; human review. |
type_mismatch | It isn't a receipt (e.g. an invoice). |
With review (poor photo, low confidence) don't reconcile automatically: see Human review.
And remember a receipt is not the deposit: check the money in your bank.
Many receipts: batches
To reconcile all pending receipts at the end of the day, use a batch of up to 100 documents.
Each document carries its own checks in items[].options, and options.export at the root builds a combined file
with one row per receipt.
Item options are merged with the common ones key by key, including inside checks: you could set expected_iban once in
the common checks and keep only what changes in each item. Here each item carries all its checks so they are easy to
read.
import { Constaia } from "@constaia/sdk";
const constaia = new Constaia();
const OUR_IBAN = process.env.OUR_IBAN;
const pending = [
{ registrationId: 123, amount: 45, reference: "INSCRIPCION 123", url: "https://files.example.com/r/123.pdf" },
{ registrationId: 124, amount: 60, reference: "INSCRIPCION 124", url: "https://files.example.com/r/124.pdf" },
];
const batch = await constaia.batches.create(
{
items: pending.map((p) => ({
fileUrl: p.url,
options: {
checks: { expectedAmount: p.amount, expectedIban: OUR_IBAN, expectedReference: p.reference },
metadata: { registration_id: String(p.registrationId) },
},
})),
options: { expect: "payment_receipt", export: ["csv", "xlsx"], metadata: { run: "2026-09-29" } },
},
{ idempotencyKey: "reconcile-2026-09-29" },
);
console.log(batch.id, batch.status); // bat_…, processingWhen it finishes you get the batch.completed webhook with counts (valid, invalid, review…), the list of
analyses and the signed URLs of exports.csv and exports.xlsx, valid for 24 hours. Read each analysis with
GET /v1/analyses/{id} and use metadata.registration_id to mark each registration as paid. The full flow, with the
webhook handler, is in Bulk processing with batches.
Test it
With a ck_test_… key, a PDF named payment_receipt.pdf (or containing receipt, justificante or transfer)
returns a 45 EUR receipt to ES7921000813610123456789 with reference INSCRIPCION 123: with the options on this page
it comes out valid with two passed iban_checksum validations. Change expected_amount to 50 to see the
expected_amount reason with error. More in Test mode.
Next steps
Sexual offences certificate (LOPIVI)
Validate the Spanish sex offender registry clearance of coaches and volunteers (recent, right holder, no records) without storing it.
Invoices to Excel
Extract number, seller, buyer, lines and VAT from PDF or photographed invoices, validate totals and tax IDs and download Excel per invoice or batch.