Factuarea API

Operaciones en lote

Contrato de éxito parcial para endpoints bulk — total, successful, failed y una lista failures por fila.

Los endpoints bulk procesan varias filas en una sola petición y nunca hacen fallar todo el lote porque una fila se rechace. Cada fila se evalúa de forma independiente y la respuesta informa, fila a fila, de si se aplicó o no. Es el contrato de éxito parcial (partial-success), compartido por todos los endpoints bulk de la API pública.

La superficie bulk ahora abarca operaciones de delete, create, pdf, send y status sobre los recursos de documento y de catálogo — no solo la familia original bulk-delete. Algunas devuelven la forma BulkPartialSuccessResult de abajo, bulk-create devuelve la forma más rica BulkCreateResult, y bulk-pdf transmite un ZIP binario en lugar del envoltorio JSON. Todas honran el éxito parcial: una fila mala nunca hunde el lote.

Forma de la respuesta

Una operación bulk siempre devuelve 200 OK con un BulkPartialSuccessResult dentro de data:

{
  "data": {
    "total": 3,
    "successful": 2,
    "failed": 1,
    "failures": [
      {
        "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
        "error_code": "resource_not_deletable",
        "error_message": "La factura ya está emitida y no se puede eliminar."
      }
    ]
  }
}
CampoTipoSignificado
totalintegerFilas procesadas (successful + failed).
successfulintegerFilas aplicadas (eliminadas, creadas o validadas).
failedintegerFilas que no se pudieron procesar. Igual a la longitud de failures.
failuresarrayUn elemento por cada fila fallida. Siempre una lista — vacía, nunca null, cuando no falló nada.

El invariante total === successful + failed y failed === failures.length se cumple siempre. Un lote totalmente correcto devuelve failures: [].

Un elemento de fallo

Cada entrada de failures identifica la fila y explica por qué se rechazó. La identidad es polimórfica:

  • id — el UUID de un recurso existente (bulk-delete y otras operaciones sobre recursos existentes).
  • index — la posición (base 0) de la fila en el lote, para filas nuevas que aún no tienen recurso (bulk-create con dry_run, import CSV).

Exactamente uno de id / index está presente.

CampoTipoSignificado
idstringUUID v7 del recurso existente que no se pudo procesar.
indexintegerPosición (base 0) de la fila dentro del lote.
error_codestringCódigo legible por máquina del catálogo de errores v1 (estable entre idiomas). Ramifica según esto.
error_messagestringMotivo legible por humanos, en español. Para mostrar, no para ramificar.
errorsarrayProblemas bloqueantes por campo (FieldIssue[]). Presentes en flujos validate-only / bulk-create; ausentes en bulk-delete.
warningsarrayAvisos no bloqueantes por campo (FieldIssue[]).

Ramifica según error_code, nunca según error_message — el mensaje es texto en español orientado a personas y puede cambiar. Para bulk-delete los códigos son resource_not_found (el UUID no existe o pertenece a otra empresa) y resource_not_deletable (el recurso existe pero su estado impide eliminarlo: un albarán firmado, un presupuesto facturado, un cliente con documentos, etc.).

Leer el resultado

No trates la llamada como todo-o-nada. Inspecciona failures y actúa fila a fila:

curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-delete \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02", "01931b3e-...a03"] }' \
  | jq '.data | {total, successful, failed, failures}'
const { data } = await factuarea.quotes.bulkDelete({ ids });

if (data.failed > 0) {
  for (const f of data.failures) {
    // f.id, f.error_code, f.error_message
    console.warn(`${f.id} → ${f.error_code}: ${f.error_message}`);
  }
}
res = factuarea.quotes.bulk_delete(ids=ids)
data = res["data"]

for f in data["failures"]:
    # ramifica según error_code, muestra error_message
    print(f["id"], f["error_code"], f["error_message"])

UUID ajenos y desconocidos

Los UUID que no pertenecen a tu empresa, o que no existen, nunca son un 404 global. El handler filtra por company_id, así que un UUID ajeno o desconocido se reporta como un fallo normal (resource_not_found) — nunca revela si un recurso existe en otro tenant.

Bulk create (solo validar con dry_run)

bulk-create acepta hasta 100 filas para facturas y hasta 500 para clientes en una sola llamada, y devuelve un envoltorio más rico, BulkCreateResult. El flag dry_run (por defecto false) alterna entre dos comportamientos:

  • dry_run: true valida cada fila sin persistir nada y devuelve un results[] por fila. Cada entrada lleva su index, un status, y los errors[] / warnings[] encontrados para esa fila. No se escribe nada — úsalo para mostrar los problemas en tu interfaz antes de confirmar.
  • dry_run: false crea solo las filas válidas. Las filas que fallan la validación no se crean y vuelven en failures[], cada una identificada por su index (base 0).

El envoltorio lleva ambos arrays, así que el mismo parser sirve en cualquier modo:

{
  "data": {
    "dry_run": true,
    "total": 2,
    "successful": 1,
    "failed": 1,
    "results": [
      { "index": 0, "status": "valid", "errors": [], "warnings": [] },
      {
        "index": 1,
        "status": "invalid",
        "errors": [{ "field": "client_id", "code": "required", "message": "El cliente es obligatorio." }],
        "warnings": []
      }
    ],
    "failures": [
      { "index": 1, "error_code": "validation_failed", "error_message": "Faltan campos obligatorios en la fila." }
    ]
  }
}

Valida primero con dry_run: true, corrige lo que marque results[] y vuelve a enviar el mismo payload con dry_run: false para persistir las filas que pasan:

curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-create \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2b7a-0e44-4c1a-8f3d-1a2b3c4d5e6f" \
  -d '{
    "dry_run": true,
    "invoices": [
      { "client_id": "01931b3e-...c01", "lines": [{ "description": "Consulting", "quantity": 1, "unit_price": "100.00", "tax_rate_id": "01931b3e-...t21" }] },
      { "lines": [{ "description": "Missing client", "quantity": 1, "unit_price": "50.00" }] }
    ]
  }' | jq '.data | {dry_run, total, successful, failed, results, failures}'

Bulk PDF (descarga ZIP)

bulk-pdf empaqueta los PDF de hasta 50 documentos en un único ZIP y transmite de vuelta el archivo binariono devuelve el envoltorio JSON. Los id que no se encuentran, o que no tienen PDF disponible, no abortan la petición: el ZIP lleva solo los documentos válidos, y los recuentos por id viajan en cabeceras de respuesta X-Bulk-* para que puedas conciliar qué entró.

curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-pdf \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"] }' \
  -D - -o invoices.zip

El flag -D - vuelca las cabeceras de respuesta: lee X-Bulk-Total, X-Bulk-Successful y X-Bulk-Failed para saber cuántos id entraron en el archivo.

Bulk send (envío en lote)

bulk-send encola hasta 200 documentos para enviarlos por email y devuelve la forma BulkPartialSuccessResult. El envío es asíncrono: una fila successful significa que el email se encoló, no que ya se entregó. Los campos opcionales to, cc, subject, message y language sobrescriben los valores por defecto para todo el lote.

curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-send \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c3e1f90-2a11-4b22-9d44-5e6f7a8b9c0d" \
  -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "language": "es" }' \
  | jq '.data | {total, successful, failed, failures}'

Transiciones de estado en lote

bulk-status mueve hasta 50 documentos a un nuevo estado, y cada transición pasa por la guarda del Aggregate — una fila cuyo estado actual prohíbe el movimiento falla de forma individual y cae en failures[], mientras el resto sí transiciona. El new_status destino debe pertenecer al conjunto cerrado permitido para ese recurso (consulta la tabla de abajo).

Para facturas, payment_date es obligatorio cuando new_status es paid. Para facturas de compra, payment_date también es obligatorio y se propaga tal cual — nunca se reemplaza en silencio por now(). Para productos y proveedores la transición es idempotente: un recurso que ya está en el estado solicitado cuenta como successful sin cambiar nada.

curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-status \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3b8d2e10-4f55-4c66-8a77-9b0c1d2e3f40" \
  -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "new_status": "paid", "payment_date": "2026-03-20" }' \
  | jq '.data | {total, successful, failed, failures}'

Los valores new_status permitidos por recurso:

Recursonew_status permitido
invoicessent, paid
quotesapproved, rejected
proformasaccepted, rejected
delivery_notesdelivered, cancelled
purchase_invoicespaid
productsactive, inactive (idempotente)
suppliersactive, inactive (idempotente)

Endpoints y límites

Cada operación limita el lote a un número fijo de filas. Trocear un trabajo mayor en bloques dentro de estos límites queda de tu parte:

OperaciónRecursosFilas máx.Forma de respuesta
bulk-createinvoices (100), clients (500)100 / 500BulkCreateResult
bulk-pdfinvoices, quotes, proformas, delivery_notes50ZIP binario + X-Bulk-*
bulk-sendinvoices, quotes, proformas, delivery_notes200BulkPartialSuccessResult
bulk-statusinvoices, quotes, proformas, delivery_notes, purchase_invoices, products, suppliers50BulkPartialSuccessResult
bulk-deletelos nueve recursosBulkPartialSuccessResult

Usa Idempotency-Key en las operaciones bulk que mutan. bulk-create, bulk-send, bulk-status y bulk-delete aceptan todas la cabecera Idempotency-Key, así que un reintento tras una conexión caída repite el resultado original en lugar de ejecutar el lote dos veces. bulk-pdf es una lectura pura y no necesita clave.

Versionado — la forma legacy

La forma de éxito parcial es el contrato actual. Los integradores anclados a una versión anterior a 2026-09-01 (mediante el header Factuarea-Version o un pin en la API key) siguen recibiendo la forma anterior de bulk-delete, de modo que ninguna integración existente se rompe:

{
  "object": "bulk_delete_result",
  "deleted": 2,
  "failed": [
    { "id": "01931b3e-...a01", "reason": "La factura ya está emitida y no se puede eliminar." }
  ]
}

El mapeo entre ambas formas es mecánico: deleted es el nuevo successful, y cada failed[].reason legacy es el nuevo failures[].error_message (la nueva forma añade encima el error_code estable y el contador total). No envíes header —o envía una fecha igual o posterior a 2026-09-01— para obtener la forma de éxito parcial.

Ancla una versión solo para congelar un contrato del que ya dependes. Las integraciones nuevas deberían usar la forma de éxito parcial: lleva un error_code estable e independiente del idioma por el que ramificar, cosa que la cadena reason legacy no ofrece.

En esta página