Idempotència
Header Idempotency-Key amb TTL de 24 h. Reintenta un POST sense duplicar recursos.
Les operacions d'escriptura (POST, PATCH, DELETE) es poden rebre
diverses vegades si la connexió es talla a mitja resposta, la teva integració
reintenta després d'un timeout, o hi ha reintents automàtics en un gateway
intermedi. Per evitar que el mateix POST creï dues factures, l'API
admet el header Idempotency-Key.
Com funciona
-
El client genera una clau única per operació (un UUID v7 és l'opció recomanada, per coherència amb els identificadors de l'API).
-
Envia-la com a header en la primera petició:
POST /v1/invoices Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b -
L'API emmagatzema el resultat (codi d'estat, headers i body) associat a aquesta clau durant 24 hores.
-
Si arriba una nova petició amb la mateixa clau dins del TTL, l'API retorna la resposta a la cau sense tornar a executar el handler.
La resposta retornada en un replay inclou el header Idempotent-Replayed: true
perquè la puguis distingir.
Format de la clau
- Una cadena opaca per al servidor: qualsevol valor únic és vàlid (UUID v7, UUID v4, ULID, nanoid, etc.).
- Longitud entre 1 i 64 caràcters.
- Recomanació: UUID v7 (
Str::uuid7(), o qualsevol generador d'UUID v7), per coherència amb els identificadors de l'API.
KEY=$(uuidgen)
curl -X POST https://api.factuarea.com/v1/invoices \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{
"client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
"series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02",
"issued_on": "2026-05-15",
"due_on": "2026-06-15",
"lines": [
{ "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" }
]
}'Automàtic amb els SDK oficials
Els SDK de TypeScript i PHP adjunten un Idempotency-Key a
cada mutació automàticament i reutilitzen la mateixa clau en els reintents d'una
crida, de manera que una petició reintentada mai no crea per duplicat.
Sobreescriu-la per crida quan vulguis deduplicació a nivell d'aplicació:
// auto-generated key
await factuarea.invoices.create(body);
// pin your own key (e.g. your order id)
await factuarea.invoices.create(body, { idempotencyKey: "order-4711" });// auto-generated key
$factuarea->invoices->publicApiV1InvoicesCreate($body);
// pin your own key
$factuarea->invoices->publicApiV1InvoicesCreate($body, idempotencyKey: 'order-4711');Empremta del payload
La clau queda lligada no només a l'Idempotency-Key, sinó també a una
empremta de la petició:
fingerprint = sha256(method + " " + path + "\n" + canonicalize(body))On canonicalize(body) és el JSON amb les claus ordenades alfabèticament.
Si reprodueixes la mateixa clau amb un payload diferent, l'API respon 409 Conflict:
{
"error": {
"type": "idempotency_error",
"code": "idempotency_key_reused",
"message": "This Idempotency-Key was previously used with a different request body.",
"request_id": "req_..."
}
}Això és una protecció contra bugs: cap caller raonable canvia el body mantenint la mateixa clau. Si necessites reintentar amb dades diferents, fes servir una clau nova.
TTL
Les entrades es persisteixen a la taula idempotency_keys durant
86.400 segons (24 h). Després d'això, les purga un schedule
diari. Si reutilitzes una clau fora de la finestra, es tracta com una
de nova.
external_id vs Idempotency-Key
Tots dos et protegeixen de duplicats, però resolen problemes diferents — i els pots fer servir junts.
Idempotency-Key | external_id | |
|---|---|---|
| Què és | Un header en un únic POST. | Una clau de negoci emmagatzemada al recurs. |
| Vida | Efímera — finestra de 24 h, després es purga. | Duradora — permanent, mai no caduca. |
| Abast | Deduplica reintents de transport d'una crida. | Deduplica per la teva pròpia clau d'integració (id d'ERP/CRM). |
| Consultable | No. | Sí — POST /v1/{recurs}/find-by-external-id. |
Fes servir l'Idempotency-Key perquè un reintent sigui segur: si la
xarxa es talla a mitja resposta, reproduir la mateixa clau dins de 24 h
retorna el resultat a la cau en lloc de crear una segona factura. Va sobre
el lliurament d'una petició.
Fes servir external_id per vincular un recurs de Factuarea amb un
registre del teu propi sistema (un id de comanda, un número de document
d'ERP). Envia'l al body de creació i l'API garanteix que és únic per empresa
(UNIQUE(company_id, external_id)). Més tard pots localitzar el recurs per
aquesta clau, sense emmagatzemar l'id de Factuarea:
curl -s -X POST https://api.factuarea.com/v1/invoices/find-by-external-id \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "external_id": "ORDER-4711" }' | jq '.data.id'En resum: Idempotency-Key és una protecció de reintent de vida curta;
external_id és el teu enllaç permanent i consultable. Una integració
típica configura tots dos — una clau nova per intent i un external_id
estable per objecte de negoci.
Recomanacions per endpoint
| Endpoint | Idempotència recomanada |
|---|---|
POST /v1/invoices | Sí (crítica) |
POST /v1/quotes | Sí |
POST /v1/clients | Sí |
POST /v1/invoices/{id}/send | Sí |
POST /v1/invoices/{id}/mark-paid | Sí |
POST /v1/invoices/{id}/payments | Sí (un pagament reintentat es comptaria dues vegades) |
POST /v1/purchase_invoices/{id}/payments | Sí (un pagament reintentat es comptaria dues vegades) |
GET /v1/... | N/A (sense efecte) |
PATCH /v1/... | Opcional (PATCH és idempotent per definició) |
DELETE /v1/... | Opcional |
Stripe documenta el mateix patró: si vens d'allà, el contracte és idèntic.
Exemple de reintent segur
import os, uuid, requests
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, max=10),
retry=retry_if_exception_type(requests.exceptions.RequestException),
)
def create_invoice(payload):
key = str(uuid.uuid4())
return requests.post(
'https://api.factuarea.com/v1/invoices',
json=payload,
headers={
'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}",
'Idempotency-Key': key,
},
timeout=30,
)async function createInvoiceWithRetry(payload, maxAttempts = 5) {
const key = crypto.randomUUID();
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const res = await fetch('https://api.factuarea.com/v1/invoices', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FACTUAREA_API_KEY}`,
'Idempotency-Key': key,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (res.ok) return res.json();
if (res.status >= 500) {
await new Promise(r => setTimeout(r, 2 ** attempt * 100));
continue;
}
return res.json();
} catch (err) {
lastError = err;
await new Promise(r => setTimeout(r, 2 ** attempt * 100));
}
}
throw lastError;
}Important: la clau ha de romandre constant en tots els reintents del
mateix POST. Si generes una clau nova en cada reintent, perds la
protecció. En l'exemple de tenacity en Python, la key es genera
fora del closure i es reutilitza en tots els reintents.
Què NO és la idempotència
- No és el mateix que el límit de peticions: una clau idempotent reproduïda dins del TTL no compta contra la teva quota; però claus diferents amb el mateix payload sí que compten, una a una.
- No substitueix un lock distribuït per la teva banda. Si dos workers creen factures de manera concurrent amb claus diferents, totes dues es persistiran; generar la clau correctament (p. ex. derivada del teu propi ID) és responsabilitat teva.
- No afecta les respostes
4xxpròpies del servidor: si la primera petició va respondre422 invalid_request_error, aquest 422 es desa a la cau. Reproduir la clau retorna el mateix 422 ambIdempotent-Replayed: true.