Constaia
Guías por caso

Conciliar justificantes de transferencia

Comprueba que cada justificante de transferencia trae el importe, el IBAN de tu cuenta y la referencia, uno a uno o en lotes con export a Excel.

Si cobras inscripciones, cuotas o licencias por transferencia, te llegan justificantes en PDF o capturas del móvil y alguien tiene que cuadrarlos con cada pago pendiente. Con el tipo payment_receipt Constaia extrae los datos del justificante y los compara con lo que esperas.

Las opciones

options.json
{
  "expect": "payment_receipt",
  "checks": {
    "expected_amount": 45,
    "expected_iban": "ES7921000813610123456789",
    "expected_reference": "INSCRIPCION 123"
  },
  "metadata": { "registration_id": "123" }
}
CheckQué comparaSi no coincide
expected_amountEl campo amount con el importe esperado (tolerancia de medio céntimo).expected_amount con error.
expected_ibanTu IBAN con los IBAN del justificante. Se ignoran mayúsculas y espacios.expected_iban con error.
expected_referenceTu código dentro de reference o de concept. Ignora mayúsculas y tildes; basta con que esté contenido.expected_reference con error.

Además, cada IBAN que aparece pasa la validación determinista iban_checksum (dígitos de control). Si uno falla, el veredicto es invalid con un motivo iban_checksum.

expected_iban no distingue ordenante y beneficiario

expected_iban da por bueno el justificante si tu IBAN aparece como beneficiary_iban o como payer_iban. Para asegurarte de que el dinero va a tu cuenta, compara también fields.beneficiary_iban.value en tu código, como en el ejemplo de abajo.

Otras opciones útiles:

  • checks.max_age_days: sobre la fecha de la transferencia (date). Por ejemplo, 30 para rechazar justificantes antiguos reutilizados.
  • checks.holder.full_name: compara con payer_name, si quieres que pague la propia persona inscrita.

Campos que obtienes

CampoEjemplo
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

Un justificante

reconcile.js
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

const constaia = new Constaia(); // lee 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,
    },
    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: "El beneficiario no es nuestra cuenta." });
  }

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

Qué falló: leer el motivo

Con invalid, el code de cada motivo con severity: "error" te dice qué no cuadra, y el message trae el valor encontrado y el esperado. Por ejemplo, si esperabas 50 € y el justificante dice 45:

{ "code": "expected_amount", "severity": "error", "message": "amount es «45» y se esperaba «50»." }
codeAcción habitual
expected_amountPago parcial o importe equivocado: pide la diferencia o revisa la tarifa aplicada.
expected_ibanLa transferencia no es a tu cuenta: rechaza.
expected_referenceFalta el código de inscripción: busca el pago por importe y ordenante o pregunta.
iban_checksumUn IBAN no pasa los dígitos de control: justificante mal leído o manipulado; revisión humana.
type_mismatchNo es un justificante (por ejemplo, una factura).

Con review (foto mala, confianza baja) no concilies automáticamente: mira Revisión humana. Y recuerda que un justificante no es el ingreso: el dinero hay que verlo en tu banco.

Muchos justificantes: lotes

Para conciliar al final del día todos los justificantes pendientes, usa un lote de hasta 100 documentos. Cada documento lleva sus propios checks en items[].options, y options.export en la raíz genera un fichero combinado con una fila por justificante.

Las opciones de cada elemento se fusionan con las comunes clave a clave, también dentro de checks: podrías poner expected_iban una sola vez en los checks comunes y dejar en cada elemento solo lo que cambia. Aquí cada elemento lleva todos sus checks para que se lean de un vistazo.

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

Cuando termina recibes el webhook batch.completed con counts (valid, invalid, review…), la lista de analyses y las URL firmadas de exports.csv y exports.xlsx, válidas 24 horas. Lee cada análisis con GET /v1/analyses/{id} y usa metadata.registration_id para marcar cada inscripción como pagada. El flujo completo, con el manejador del webhook, está en Procesamiento masivo con lotes.

Probarlo

Con una clave ck_test_…, un PDF llamado payment_receipt.pdf (o que contenga receipt, justificante o transfer) devuelve un justificante de 45 EUR a ES7921000813610123456789 con referencia INSCRIPCION 123: con las opciones de esta página sale valid y dos validaciones iban_checksum superadas. Cambia expected_amount a 50 para ver el motivo expected_amount con error. Más en Modo test.

Siguientes pasos

En esta página