Verifactu Direct API · batches, status, errors and idempotency
Operational details around registration: batch submission, tracking the AEAT decision, errors and duplicate prevention.
Batch registration
You can register up to 75 invoices in one POST /external-invoices/bulk request. The operation is all-or-nothing: if one invoice fails validation or is a duplicate, none are registered, so your Veri*Factu chain has no gaps. Each invoice accepts the same fields as an individual registration.
curl -X POST https://api.test.bilteo.com/api/v1/external-invoices/bulk \
-H "Authorization: Bearer bk_live_xxx" -H "Content-Type: application/json" \
-d '{ "facturas": [
{ "serie": "FV", "numero": "1070", "fecha_expedicion": "2026-04-15",
"tipo_factura": "F1", "descripcion": "Monthly service", "nif": "A39208155",
"nombre": "Gráficas Belmar SA",
"lineas": [ { "base_imponible": "120", "tipo_impositivo": "21", "cuota_repercutida": "25.20" } ],
"importe_total": "145.20" },
{ "serie": "TPV", "numero": "5588", "fecha_expedicion": "2026-04-15",
"tipo_factura": "F2", "descripcion": "In-store sale",
"lineas": [ { "base_imponible": "40", "tipo_impositivo": "10", "cuota_repercutida": "4" } ],
"importe_total": "44" }
] }'
# → 201, { "facturas": [ { "id": "EXT…", "numero": "1070", "verifactu_record_id": "VF…" }, … ] }Registration is synchronous; submission to the AEAT happens later, just like an individual registration. Split volumes of thousands of invoices into batches or contact us.
Track the status
Registration is asynchronous: it starts as pending and the AEAT responds later. Receive the decision through the verifactu.accepted / verifactu.rejected / verifactu.error webhooks, or retrieve it using the verifactu_record_id:
# Veri*Factu record (decision details)
curl https://api.test.bilteo.com/api/v1/verifactu-records/VF… \
-H "Authorization: Bearer bk_live_xxx"
# The invoice: its `state` is synchronized with the AEAT decision
# (pending → registered | rejected | cancelled)
curl https://api.test.bilteo.com/api/v1/external-invoices/EXT… \
-H "Authorization: Bearer bk_live_xxx"
# And the filtered list:
curl "https://api.test.bilteo.com/api/v1/external-invoices?state=rejected" \
-H "Authorization: Bearer bk_live_xxx"Errors
| Code | When it occurs |
|---|---|
403 permission_error | The vf_direct_api.enabled gate is disabled or the key lacks verifactu:write. |
409 conflict_error | You already registered an invoice with the same (serie, numero) or external_id, or a retry with the same Idempotency-Key is in progress. |
400 validation_error | A field is missing, a date is not ISO, or the tax breakdown is inconsistent. |
422 verifactu_error | The breakdown is fiscally invalid or, with validar_cliente, the recipient tax ID is not registered in the AEAT census. |
Idempotency
Two complementary mechanisms are available:
Idempotency-Key(header): prevents an invoice registration or cancellation retry from duplicating the record. Send a unique opaque key for each operation; retrying with the same key and body replays the original response.bashcurl -X POST https://api.test.bilteo.com/api/v1/external-invoices \ -H "Authorization: Bearer bk_live_xxx" -H "Content-Type: application/json" \ -H "Idempotency-Key: 7e9f…-uuid-per-operation" \ -d '{ … }'external_id(field): a permanent link to your system. A second registration with the sameexternal_idreturns409; this provides long-term duplicate protection and lets you retrieve it later (GET /external-invoices?external_id=…).