Factuarea API

Operacions en lot

Contracte d'èxit parcial per a endpoints bulk — total, successful, failed i una llista failures per fila.

Els endpoints bulk processen diverses files en una sola petició i mai no fan fallar tot el lot perquè una fila es rebutgi. Cada fila s'avalua de manera independent i la resposta informa, fila a fila, de si s'ha aplicat o no. És el contracte d'èxit parcial (partial-success), compartit per tots els endpoints bulk de l'API pública.

La superfície bulk ara abasta operacions de delete, create, pdf, send i status sobre els recursos de document i de catàleg — no només la família original bulk-delete. Algunes retornen la forma BulkPartialSuccessResult de sota, bulk-create retorna la forma més rica BulkCreateResult, i bulk-pdf transmet un ZIP binari en lloc de l'embolcall JSON. Totes honoren l'èxit parcial: una fila dolenta mai no enfonsa el lot.

Forma de la resposta

Una operació bulk sempre retorna 200 OK amb un BulkPartialSuccessResult dins 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."
      }
    ]
  }
}
CampTipusSignificat
totalintegerFiles processades (successful + failed).
successfulintegerFiles aplicades (eliminades, creades o validades).
failedintegerFiles que no s'han pogut processar. Igual a la longitud de failures.
failuresarrayUn element per cada fila fallida. Sempre una llista — buida, mai null, quan no ha fallat res.

L'invariant total === successful + failed i failed === failures.length es compleix sempre. Un lot totalment correcte retorna failures: [].

Un element d'error

Cada entrada de failures identifica la fila i explica per què s'ha rebutjat. La identitat és polimòrfica:

  • id — l'UUID d'un recurs existent (bulk-delete i altres operacions sobre recursos existents).
  • index — la posició (base 0) de la fila al lot, per a files noves que encara no tenen recurs (bulk-create amb dry_run, import CSV).

Exactament un d'id / index és present.

CampTipusSignificat
idstringUUID v7 del recurs existent que no s'ha pogut processar.
indexintegerPosició (base 0) de la fila dins del lot.
error_codestringCodi llegible per màquina del catàleg d'errors v1 (estable entre idiomes). Ramifica segons això.
error_messagestringMotiu llegible per humans, en castellà. Per mostrar, no per ramificar.
errorsarrayProblemes bloquejants per camp (FieldIssue[]). Presents en fluxos validate-only / bulk-create; absents en bulk-delete.
warningsarrayAvisos no bloquejants per camp (FieldIssue[]).

Ramifica segons error_code, mai segons error_message — el missatge és text en castellà orientat a persones i pot canviar. Per a bulk-delete els codis són resource_not_found (l'UUID no existeix o pertany a una altra empresa) i resource_not_deletable (el recurs existeix però el seu estat impedeix eliminar-lo: un albarà signat, un pressupost facturat, un client amb documents, etc.).

Llegir el resultat

No tractis la crida com a tot-o-res. Inspecciona failures i actua 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 segons error_code, mostra error_message
    print(f["id"], f["error_code"], f["error_message"])

UUID aliens i desconeguts

Els UUID que no pertanyen a la teva empresa, o que no existeixen, mai no són un 404 global. El handler filtra per company_id, així que un UUID alien o desconegut es reporta com un error normal (resource_not_found) — mai no revela si un recurs existeix en un altre tenant.

Bulk create (només validar amb dry_run)

bulk-create accepta fins a 100 files per a factures i fins a 500 per a clients en una sola crida, i retorna un embolcall més ric, BulkCreateResult. El flag dry_run (per defecte false) alterna entre dos comportaments:

  • dry_run: true valida cada fila sense persistir res i retorna un results[] per fila. Cada entrada porta el seu index, un status, i els errors[] / warnings[] trobats per a aquella fila. No s'escriu res — fes-lo servir per mostrar els problemes a la teva interfície abans de confirmar.
  • dry_run: false crea només les files vàlides. Les files que fallen la validació no es creen i tornen a failures[], cadascuna identificada pel seu index (base 0).

L'embolcall porta tots dos arrays, així que el mateix parser serveix en qualsevol mode:

{
  "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 primer amb dry_run: true, corregeix el que marqui results[] i torna a enviar el mateix payload amb dry_run: false per persistir les files que passen:

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 (descàrrega ZIP)

bulk-pdf empaqueta els PDF de fins a 50 documents en un únic ZIP i transmet de tornada l'arxiu binarino retorna l'embolcall JSON. Els id que no es troben, o que no tenen PDF disponible, no aborten la petició: el ZIP porta només els documents vàlids, i els recomptes per id viatgen a les capçaleres de resposta X-Bulk-* perquè puguis conciliar què va entrar.

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 - bolca les capçaleres de resposta: llegeix X-Bulk-Total, X-Bulk-Successful i X-Bulk-Failed per saber quants id van entrar a l'arxiu.

Bulk send (enviament en lot)

bulk-send encua fins a 200 documents per enviar-los per email i retorna la forma BulkPartialSuccessResult. L'enviament és asíncron: una fila successful significa que l'email es va encuar, no que ja s'ha entregat. Els camps opcionals to, cc, subject, message i language sobreescriuen els valors per defecte per a tot el lot.

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

Transicions d'estat en lot

bulk-status mou fins a 50 documents a un nou estat, i cada transició passa per la guarda de l'Aggregate — una fila l'estat actual de la qual prohibeix el moviment falla de manera individual i cau a failures[], mentre que la resta sí que transiciona. El new_status destí ha de pertànyer al conjunt tancat permès per a aquell recurs (consulta la taula de sota).

Per a factures, payment_date és obligatori quan new_status és paid. Per a factures de compra, payment_date també és obligatori i es propaga tal qual — mai no es reemplaça en silenci per now(). Per a productes i proveïdors la transició és idempotent: un recurs que ja està en l'estat sol·licitat compta com a successful sense canviar res.

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

Els valors new_status permesos per recurs:

Recursnew_status permès
invoicessent, paid
quotesapproved, rejected
proformasaccepted, rejected
delivery_notesdelivered, cancelled
purchase_invoicespaid
productsactive, inactive (idempotent)
suppliersactive, inactive (idempotent)

Endpoints i límits

Cada operació limita el lot a un nombre fix de files. Trossejar una feina més gran en blocs dins d'aquests límits queda de la teva part:

OperacióRecursosFiles màx.Forma de resposta
bulk-createinvoices (100), clients (500)100 / 500BulkCreateResult
bulk-pdfinvoices, quotes, proformas, delivery_notes50ZIP binari + X-Bulk-*
bulk-sendinvoices, quotes, proformas, delivery_notes200BulkPartialSuccessResult
bulk-statusinvoices, quotes, proformas, delivery_notes, purchase_invoices, products, suppliers50BulkPartialSuccessResult
bulk-deleteels nou recursosBulkPartialSuccessResult

Fes servir Idempotency-Key a les operacions bulk que muten. bulk-create, bulk-send, bulk-status i bulk-delete accepten totes la capçalera Idempotency-Key, així que un reintent després d'una connexió caiguda repeteix el resultat original en lloc d'executar el lot dues vegades. bulk-pdf és una lectura pura i no necessita clau.

Versionat — la forma legacy

La forma d'èxit parcial és el contracte actual. Els integradors ancorats a una versió anterior a 2026-09-01 (mitjançant el header Factuarea-Version o un pin a l'API key) continuen rebent la forma anterior de bulk-delete, de manera que cap integració existent no es trenca:

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

El mapatge entre ambdues formes és mecànic: deleted és el nou successful, i cada failed[].reason legacy és el nou failures[].error_message (la nova forma hi afegeix a sobre l'error_code estable i el comptador total). No enviïs header —o envia una data igual o posterior a 2026-09-01— per obtenir la forma d'èxit parcial.

Ancora una versió només per congelar un contracte del qual ja depens. Les integracions noves haurien d'usar la forma d'èxit parcial: porta un error_code estable i independent de l'idioma pel qual ramificar, cosa que la cadena reason legacy no ofereix.

En aquesta pàgina