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."
}
]
}
}| Camp | Tipus | Significat |
|---|---|---|
total | integer | Files processades (successful + failed). |
successful | integer | Files aplicades (eliminades, creades o validades). |
failed | integer | Files que no s'han pogut processar. Igual a la longitud de failures. |
failures | array | Un 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 ambdry_run, import CSV).
Exactament un d'id / index és present.
| Camp | Tipus | Significat |
|---|---|---|
id | string | UUID v7 del recurs existent que no s'ha pogut processar. |
index | integer | Posició (base 0) de la fila dins del lot. |
error_code | string | Codi llegible per màquina del catàleg d'errors v1 (estable entre idiomes). Ramifica segons això. |
error_message | string | Motiu llegible per humans, en castellà. Per mostrar, no per ramificar. |
errors | array | Problemes bloquejants per camp (FieldIssue[]). Presents en fluxos validate-only / bulk-create; absents en bulk-delete. |
warnings | array | Avisos 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: truevalida cada fila sense persistir res i retorna unresults[]per fila. Cada entrada porta el seuindex, unstatus, i elserrors[]/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: falsecrea només les files vàlides. Les files que fallen la validació no es creen i tornen afailures[], cadascuna identificada pel seuindex(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 binari — no 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.zipEl 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:
| Recurs | new_status permès |
|---|---|
invoices | sent, paid |
quotes | approved, rejected |
proformas | accepted, rejected |
delivery_notes | delivered, cancelled |
purchase_invoices | paid |
products | active, inactive (idempotent) |
suppliers | active, 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ó | Recursos | Files màx. | Forma de resposta |
|---|---|---|---|
bulk-create | invoices (100), clients (500) | 100 / 500 | BulkCreateResult |
bulk-pdf | invoices, quotes, proformas, delivery_notes | 50 | ZIP binari + X-Bulk-* |
bulk-send | invoices, quotes, proformas, delivery_notes | 200 | BulkPartialSuccessResult |
bulk-status | invoices, quotes, proformas, delivery_notes, purchase_invoices, products, suppliers | 50 | BulkPartialSuccessResult |
bulk-delete | els nou recursos | — | BulkPartialSuccessResult |
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.