Constaia
Use-case guides

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.

Esta página ainda não está traduzida para o seu idioma. Mostramos a versão em inglês.

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

options.json
{
  "expect": "payment_receipt",
  "checks": {
    "expected_amount": 45,
    "expected_iban": "ES7921000813610123456789",
    "expected_reference": "INSCRIPCION 123"
  },
  "metadata": { "registration_id": "123" }
}
CheckWhat it comparesIf it doesn't match
expected_amountThe amount field with the expected amount (half-cent tolerance).expected_amount with error.
expected_ibanYour IBAN with the IBANs on the receipt. Case and spaces are ignored.expected_iban with error.
expected_referenceYour 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 example 30 to reject old, reused receipts.
  • checks.holder.full_name: compares with payer_name, if the registrant must pay themselves.

Fields you get

FieldExample
amount, currency45, EUR
date2026-09-20
payer_name, payer_ibanMARÍA GARCÍA LÓPEZ, ES9121000418450200051332
beneficiary_name, beneficiary_ibanCLUB DEPORTIVO ARCO MADRID, ES7921000813610123456789
concept, referenceINSCRIPCION 123

One receipt

reconcile.js
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”." }
codeUsual action
expected_amountPartial payment or wrong amount: ask for the difference or check the fee applied.
expected_ibanThe transfer isn't to your account: reject.
expected_referenceThe registration code is missing: look for the payment by amount and payer, or ask.
iban_checksumAn IBAN fails its check digits: misread or tampered receipt; human review.
type_mismatchIt 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.

reconcile-batch.js
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_…, processing

When 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

Nesta página