VerifikaDocs
Guías

Conciliación por API

Sube el extracto del banco, mira qué cuadra con el libro, resuelve lo que falta en lote y descarga el reporte — lo mismo que la pantalla de Conciliación, desde tu ERP o tu bot.

La conciliación compara el extracto del banco contra el libro de Verifika: cada movimiento del banco que ya está en el libro queda conciliado; lo que falta se registra o se ignora; y lo que está en el libro pero el banco no muestra queda a la vista. Es lo mismo que Contabilidad → Conciliación bancaria del panel, con las mismas cifras y las mismas reglas: lo que resuelvas por API se ve allí, y al revés.

1. Subir el extracto ─► 2. Esperar la lectura ─► 3. Ver el resumen ─► 4. Resolver filas ─► 5. Reporte
   POST /bank-statements   202 → GET o webhook      verdict, progress     /lines, /lines/bulk     /report

Antes de empezar

  • El plan incluye la conciliación bancaria (features.bank_reconciliation en GET /v1/plan). Si no, 403 PLAN_FEATURE_REQUIRED con details.feature: "bank_reconciliation".
  • La llave tiene los permisos: reconciliation.read (ver), reconciliation.write (subir y resolver) y, si vas a borrar extractos, reconciliation.delete. Para registrar una salida del banco como gasto hace falta además expenses.write. Crear la llave.
  • Con una llave real. La conciliación no tiene modo prueba: con una vk_test_ responde 403 TEST_MODE_UNAVAILABLE.

1. Sube el extracto

Tres formas, una sola por petición, y el archivo hasta 15 MB:

FormaCómo
Archivomultipart/form-data con el campo file; los demás campos van como texto.
Base64JSON con fileBase64 y fileName (con extensión).
URLJSON con fileUrl: https y pública. La bajamos con un tope de 20 s.

Leemos PDF (también con clave), Excel (.xlsx, .xls), CSV, TXT, XML, .prn, .dat, el .fil de Bancolombia y un .zip con uno de esos adentro (como los entrega la sucursal virtual de Bancolombia).

# Por archivo
curl -X POST https://api.verifika.tech/v1/bank-statements \
  -H "Authorization: Bearer $VERIFIKA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@extracto-septiembre.pdf" \
  -F "paymentAccountId=cmt7xbfs10005q3w1u8k2c9va"

# Por URL
curl -X POST https://api.verifika.tech/v1/bank-statements \
  -H "Authorization: Bearer $VERIFIKA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "fileUrl": "https://drive.tuerp.com/extractos/2026-09.pdf" }'
CampoPara qué
paymentAccountIdLa cuenta de recaudo del extracto (GET /v1/payment-accounts). Opcional: si no la mandas, se deduce por el banco y los últimos 4 dígitos.
passwordLa clave del PDF. Antes de pedirla probamos las que el negocio pidió recordar.
rememberPasswordtrue para guardarla (cifrada) para los próximos extractos de esa cuenta.
asynctrue para que conteste 202 de una vez, sin esperar la lectura.

2. Espera la lectura: 201 o 202

Un CSV se lee en milisegundos; un PDF escaneado puede tomar un minuto. La petición espera hasta 20 segundos:

  • 201: el extracto ya está leído y cruzado. Sigue al paso 3.
  • 202: se sigue leyendo. Llega status: "processing", la cabecera Location con su URL y Retry-After: 10. El id del 202 es el id del extracto de punta a punta.

Para saber cuándo termina tienes dos caminos:

while (statement.status === 'processing') {
  await new Promise((r) => setTimeout(r, 10_000)); // respeta Retry-After
  statement = await (await fetch(`${API}/bank-statements/${statement.id}`, { headers })).json();
}
if (statement.status === 'failed') throw new Error(`${statement.error.code}: ${statement.error.message}`);

O, mejor, suscribe los webhooks bank_statement.processed (trae el extracto completo) y bank_statement.failed (trae el error). processed sale para cualquier extracto que termine de leerse, también los que el negocio sube desde el panel.

Si la lectura falla a tiempo, el error sale en la misma respuesta de la subida (y no queda nada guardado). Si falla después del 202, GET devuelve status: "failed" con el mismo error que habría dado la subida. Una lectura que lleva más de 15 minutos se cortó: pasa a failed con PROCESSING_INTERRUPTED («vuelve a subirlo»).

3. Mira el resumen

El extracto leído (recortado)
{
  "object": "bankStatement",
  "id": "cmuh8q2r70044x8w1b6n3m1kd",
  "status": "processed",
  "fileName": "extracto-septiembre.pdf",
  "bank": { "key": "bancolombia", "name": "Bancolombia" },
  "accountLast4": "8300",
  "periodStart": "2026-09-01",
  "periodEnd": "2026-09-30",
  "verdict": "VERIFIED",
  "balances": { "opening": 1250000, "closing": 3184520.37 },
  "totals": { "credits": 4872300, "debits": 2937779.63, "lineCount": 64 },
  "difference": { "closingBalance": 0, "credits": 0, "debits": 0 },
  "progress": { "total": 64, "resolved": 51, "pending": 13, "percentResolved": 80 },
  "groups": {
    "matched": { "count": 38, "amount": 4104300 },
    "registered": { "count": 0, "amount": 0 },
    "bankFees": { "count": 13, "amount": 26443.62 },
    "ignored": { "count": 0, "amount": 0 },
    "missingIncomes": { "count": 6, "amount": 768000 },
    "missingExpenses": { "count": 7, "amount": 1843200 },
    "onlyInVerifika": { "count": 2, "amount": 95000 }
  }
}
CampoQué dice
verdictVERIFIED: lo que leímos cuadra al centavo con el resumen del banco. MISMATCH: no cuadra — no es un error, el extracto se guarda igual; mira difference (lo leído menos lo del banco, en pesos) y checks. UNVERIFIABLE: el archivo no trae saldos contra qué medirlo (algunos CSV y TXT).
progressCuántas filas están resueltas y cuántas faltan.
groups.missingIncomes / missingExpensesEntradas y salidas del banco que faltan en Verifika: lo que hay que resolver.
groups.bankFeesCobros del banco reconocidos solos: 4×1000, cuota de manejo, comisiones, intereses.
groups.onlyInVerifikaIngresos del libro en esa cuenta y período que el banco no muestra. Vale la pena mirarlos.

4. Resuelve las filas

curl "https://api.verifika.tech/v1/bank-statements/cmuh8q2r70044x8w1b6n3m1kd/lines?status=UNMATCHED&direction=in" \
  -H "Authorization: Bearer $VERIFIKA_API_KEY"

Cada fila trae amount con signo (+ entró, − salió), direction, status (UNMATCHED = falta en Verifika) y, si ya quedó resuelta, el income o expense con el que quedó. Filtros: status (separados por coma), direction, from, to, minAmount, maxAmount, q y category.

Hay cuatro acciones por fila:

AcciónQué hace
POST …/lines/{lineId}/match con { "incomeId" } o { "expenseId" }La concilia con algo que ya está en el libro (mismo valor exacto).
POST …/lines/{lineId}/registerLa registra: una entrada como ingreso (PENDING, «lo vio el banco»); una salida como gasto, con categoryId opcional (pide expenses.write).
POST …/lines/{lineId}/ignore«No va en el libro»: un traslado entre tus propias cuentas, por ejemplo.
POST …/lines/{lineId}/resetDeshace lo resuelto y la vuelve a cruzar. No borra lo que se registró en el libro.

En lote, con remaining

Para resolver muchas a la vez está POST …/lines/bulk, con lineIds (hasta 1.000) o un filter (los mismos campos del listado, con status como arreglo; {} = todas). Por filtro se toman las primeras 1.000 que tu llave puede resolver, y remaining dice cuántas quedan: repite el pedido hasta que llegue a 0.

// Registrar todas las entradas que faltan en el libro.
let r;
do {
  r = await (
    await fetch(`${API}/bank-statements/${statement.id}/lines/bulk`, {
      method: 'POST',
      headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() },
      body: JSON.stringify({ action: 'register_income', filter: { status: ['UNMATCHED'], direction: 'in' } }),
    })
  ).json();
  console.log(`Registradas ${r.applied}, saltadas ${r.skipped.length}, faltan ${r.remaining}`);
} while (r.remaining > 0 && r.applied > 0);
Respuesta del lote
{
  "object": "bankStatementBulkAction",
  "action": "register_income",
  "selected": 6,
  "applied": 5,
  "alreadyDone": 0,
  "skipped": [
    { "lineId": "cmuh8q2t10046x8w1y7u8i9op", "code": "LINE_NOT_REGISTRABLE", "message": "Un pago a la tarjeta no es un ingreso del negocio." }
  ],
  "remaining": 0,
  "created": { "incomes": 5, "expenses": 0 },
  "statement": { "id": "cmuh8q2r70044x8w1b6n3m1kd", "progress": { "total": 64, "resolved": 56, "pending": 8, "percentResolved": 88 } }
}

Las acciones del lote son register, register_income, register_expense, ignore y reset. Lo que no aplica a una fila (ya estaba registrada, es de otra dirección…) sale en skipped con su código y no tumba el resto. reset por filtro admite hasta 1.000 filas; con más, 422 BULK_TOO_LARGE: manda lineIds en tandas.

Reintentar sin duplicar

Cada acción mira el estado de la fila antes de escribir. Repetirla contesta outcome: "ALREADY_DONE" sin tocar nada: un reintento nunca crea un segundo ingreso. En la subida, la protección es doble: la Idempotency-Key y que el mismo archivo no se puede subir dos veces (409 STATEMENT_DUPLICATE con details.statementId, que es el que ya existe).

Después de registrar en el libro movimientos que faltaban por otro lado, POST …/rematch vuelve a cruzar el extracto. Lo resuelto a mano se respeta.

5. Descarga el reporte

curl -o conciliacion.xlsx \
  "https://api.verifika.tech/v1/bank-statements/cmuh8q2r70044x8w1b6n3m1kd/report?format=xlsx" \
  -H "Authorization: Bearer $VERIFIKA_API_KEY"

Es el mismo «Descargar reporte» del panel: Excel con las hojas Resumen, Movimientos y Solo en Verifika; también format=pdf o format=csv.

Errores de la subida

HTTPcodeQué pasó
409STATEMENT_DUPLICATEEse archivo ya se subió (details.statementId; con details.status: "processing" si todavía se está leyendo).
422STATEMENT_PASSWORD_REQUIRED · STATEMENT_PASSWORD_INCORRECTEl PDF tiene clave: mándala en password. details.savedTried dice cuántas guardadas se probaron.
422STATEMENT_NOT_A_STATEMENTNo es un extracto (details.documentLabel dice qué parece: una factura, un comprobante…).
422STATEMENT_NO_TABLE · STATEMENT_SCANNED · STATEMENT_UNREADABLENo hay tabla de movimientos, es un PDF escaneado y con clave, o el PDF está dañado.
422STATEMENT_ZIP_UNREADABLEEl .zip no trae un solo extracto legible (o tiene clave, o trae otro .zip adentro).
415STATEMENT_UNSUPPORTED · STATEMENT_XLS_LEGACY · FILE_TYPE_NOT_ALLOWEDFormato que no leemos.
413FILE_TOO_LARGE · STATEMENT_TOO_LARGEMás de 15 MB.
429STATEMENT_UPLOADS_IN_PROGRESSYa hay 3 extractos leyéndose en este negocio. Espera a que termine alguno.
503SERVICE_BUSYEstamos leyendo muchos extractos a la vez. Reintenta en un minuto.
409STATEMENT_PROCESSINGPediste filas, el reporte o una acción de un extracto que todavía se está leyendo.

Y los de una fila: LINE_ALREADY_REGISTERED, LINE_ALREADY_MATCHED, LINE_IGNORED (409), LINE_DIRECTION_MISMATCH, LINE_NOT_REGISTRABLE, AMOUNT_MISMATCH (422) y EXPENSES_NOT_ALLOWED (403). La referencia de cada endpoint está en Conciliación bancaria.

Borrar un extracto

DELETE /v1/bank-statements/{id} (permiso reconciliation.delete) borra el extracto y sus filas. Lo que se registró en el libro desde él se queda.

Por MCP

Las mismas operaciones existen como herramientas del servidor MCP: upload_bank_statement, get_bank_statement, list_bank_statement_lines, bulk_bank_statement_lines y las demás. Así un asistente puede conciliar el mes conversando.

En esta página