Factuarea APIDevelopers

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 fa servir el header Idempotency-Key.

Idempotency-Key és obligatori a les operacions la repetició de les quals pot lliurar un efecte extern, de pagament o irreversible. Aquestes operacions marquen el header com a required: true i documenten 422 idempotency_key_required a OpenAPI. A les escriptures internes reversibles continua sent opcional, tot i que recomanat.

Com funciona

  1. El client genera una clau única per operació (un UUID v7 és l'opció recomanada, per coherència amb els identificadors de l'API).

  2. Envia-la com a header en la primera petició:

    POST /v1/invoices
    Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b
  3. L'API emmagatzema el resultat (codi d'estat, headers i body) associat a aquesta clau durant 24 hores.

  4. 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 255 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-Keyexternal_id
Què ésUn header en un únic POST.Una clau de negoci emmagatzemada al recurs.
VidaEfímera — finestra de 24 h, després es purga.Duradora — permanent, mai no caduca.
AbastDeduplica reintents de transport d'una crida.Deduplica per la teva pròpia clau d'integració (id d'ERP/CRM).
ConsultableNo.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.

Obligatòria o recomanada

El requisit es deriva de l'operació, no només del seu mètode HTTP:

Classe d'operacióContracte del header
Escriptures de correu, lot, exportació, pujada, IA/tercers de pagament, declaracions fiscals o facturacióObligatori
Qualsevol operació amb x-irreversible: trueObligatori
Escriptures internes reversibles, com crear o editar un esborrany o registre de catàlegOpcional, recomanat
POST de només lectura com find-by-*, càlculs i previsualitzacionsNo s'aplica
Operacions GETNo s'aplica

No mantinguis aquesta classificació com una llista fixa a la teva integració. Llegeix el valor Idempotency-Key.required de l'operació al contracte OpenAPI, perquè els endpoints nous heretin el comportament correcte.

Quan una operació obligatòria es crida sense el header després que es tanqui la seva finestra de transició, l'API la rebutja abans d'executar la lògica de negoci:

{
  "error": {
    "type": "invalid_request_error",
    "code": "idempotency_key_required",
    "param": "Idempotency-Key",
    "message": "Esta operación exige la cabecera Idempotency-Key. Envía una clave única por operación (un UUID sirve) y, si reintentas, repite exactamente la misma clave."
  }
}

Durant una finestra de transició anunciada, la mateixa petició encara es pot acceptar i porta Deprecation: true; Sunset apareix quan ja s'ha programat un tall específic per a l'entorn. Les credencials de prova i producció poden tenir talls diferents per migrar primer al sandbox.

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

def create_invoice(payload):
    key = str(uuid.uuid4())

    @retry(
        stop=stop_after_attempt(5),
        wait=wait_exponential(multiplier=1, max=10),
        retry=retry_if_exception_type(requests.exceptions.RequestException),
    )
    def attempt():
        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,
        )

    return attempt()
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 mateixa operació. Si generes una clau nova en cada reintent, perds la protecció. En l'exemple de tenacity en Python, la funció exterior genera la key i el closure attempt reintentat la reutilitza.

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 4xx pròpies del servidor: si la primera petició va respondre 422 invalid_request_error, aquest 422 es desa a la cau. Reproduir la clau retorna el mateix 422 amb Idempotent-Replayed: true.

Diagnòstic de botigues i MCP

Els POST de prova de connexió i les operacions de desconnexió de botigues declaren Idempotency-Key a REST. Assigna una clau a cada diagnòstic previst i reutilitza-la només per als seus reintents; una comprovació posterior nova necessita una altra clau. A MCP l’argument és idempotency_key i la política d’eines irreversibles pot exigir-lo encara que discovery marqui el camp opcional. Consulta el protocol MCP.

En aquesta pàgina

Et donem un cop de mà?Contactar amb suport