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/companies/{company}/invoices
    Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b

    Totes les rutes d'aquesta guia pengen de /v1/companies/{company}/…: {company} és l'id (UUID v7) del NIF amb què operes, i als exemples curl és $COMPANY_ID. Arribar a un NIF explica com resoldre'l.

  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/companies/$COMPANY_ID/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 422 Unprocessable Entity:

{
  "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.Sí — POST /v1/companies/{company}/{resource}/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/companies/$COMPANY_ID/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.

Cobrament desatès: external_id és la protecció de reintent

Un terminal que emet una factura simplificada ja cobrada (un POST /v1/companies/{company}/invoices amb type: F2, un bloc payment o options.register_verifactu; consulta Facturació des de terminals desatesos) pot haver de reintentar dies després del primer intent, quan una clau de 24 hores ja es va purgar fa temps. Per a aquest flux, external_id fa de clau d'idempotència i no caduca mai. Només es comporten així les peticions que són cobraments desatesos; qualsevol altre POST /v1/companies/{company}/invoices conserva l'upsert per external_id descrit a dalt.

Quan l'external_id ja pertany a una factura emesa a la teva empresa, la petició és una repetició:

SituacióResposta
Mateix external_id, mateix tipus i total200 amb Idempotent-Replayed: true i la factura ja emesa. Abans de respondre, l'API completa el que faltava (emetre un esborrany, registrar el pagament, generar l'alta VeriFactu). No crea res de nou, no consumeix número i no torna a enviar per email una factura l'email de la qual ja és a la cua o lliurat.
Mateix external_id, tipus o total diferents409 unattended_replay_mismatch, error.type: idempotency_error, subcode: unattended_replay_mismatch i param: external_id. No es canvia res. No és l'error d'una Idempotency-Key reutilitzada (422 idempotency_key_reused): té el seu propi codi i el seu propi status, així que tracta'l a part. L'external_id identifica UNA operació, així que fes-ne servir un de nou per a una operació nova.
Una altra petició amb el mateix external_id continua en cursLa segona petició espera fins a 20 segons la primera i després la repeteix. Si la primera no ha acabat, respon 409 resource_locked amb param: external_id i no ha escrit res: envia la mateixa petició una altra vegada.

Dues peticions simultànies amb el mateix external_id produeixen, per tant, una factura, un número i un pagament.

{
  "error": {
    "type": "idempotency_error",
    "code": "unattended_replay_mismatch",
    "subcode": "unattended_replay_mismatch",
    "message": "La operación con external_id «KIOSK-0042-20260601-000187» ya está emitida con un tipo o un total distintos y no puede reenviarse con datos diferentes.",
    "param": "external_id",
    "doc_url": "https://docs.factuarea.com/guides/errors#unattended_replay_mismatch",
    "request_id": "req_01J8Y3M2Q4CF0000000000URM1"
  }
}

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.

Biblioteca PDF, vistes prèvies i artefactes

Les operacions de la biblioteca de plantilles PDF i de les vistes prèvies i artefactes PDF segueixen la mateixa regla: la família de l’operació decideix el contracte.

OperacióIdempotency-KeyFamília
POST /v1/companies/{company}/pdf-template-previewsObligatòriaExportació
POST /v1/companies/{company}/pdf-artifactsObligatòriaExportació
POST /v1/companies/{company}/pdf-artifacts/{artifact}/renderObligatòriaExportació
POST /v1/companies/{company}/pdf-templates/assetsObligatòriaPujada
POST i PATCH sobre /v1/companies/{company}/pdf-templates i /v1/companies/{company}/pdf-commercial-identities (crear, canviar el nom, revisions, duplicar, arxivar, restaurar)Opcional, recomanadaEscriptura de document
PATCH /v1/companies/{company}/pdf-templates/defaults i PATCH /v1/companies/{company}/pdf-templates/contact-defaults/...Opcional, recomanadaEscriptura de document
PATCH /v1/companies/{company}/series/{series}/pdf-templateOpcional, recomanadaEscriptura de document
PATCH .../presentation i POST .../presentation-revisions de factures, pressupostos, factures proforma i albarans, i POST /v1/companies/{company}/invoices/{invoice}/administrative-revisionsOpcional, recomanadaEscriptura de document

Les quatre operacions obligatòries retornen 422 idempotency_key_required sense la capçalera, com qualsevol altra operació obligatòria. Les lectures no porten clau. Recorda que a les opcionals un POST repetit sense clau repeteix el seu efecte.

El que les distingeix:

  • Repetició abans de les condicions. La resposta desada es retorna abans d’If-Match, expected_version i les altres comprovacions de versió, i abans de generar cap identificador. Un reintent d’un PATCH que ja ha reeixit rep l’èxit original amb Idempotent-Replayed: true, no un 412 o 409 provocat pel teu propi primer intent. L’autoritat de qui crida es verifica abans: si s’ha revocat el rol o l’accés, reps l’error d’autorització, no la resposta desada.
  • Cos exacte a les tres operacions de PDF. A pdf-template-previews, pdf-artifacts i render, l’empremta es calcula sobre el cos cru tal com el vas enviar. La mateixa clau amb un cos que difereix en un sol byte (també l’ordre de les claus i els espais) retorna 422 idempotency_key_reused. Dues claus diferents amb cossos equivalents porten al mateix artefacte.
  • Pujada multipart. A POST /v1/companies/{company}/pdf-templates/assets l’empremta cobreix els camps del formulari i un resum del fitxer, així que un reintent ha d’enviar el mateix fitxer.
  • Una clau nova per a un intent nou. La resposta desada de la primera crida es repeteix durant 24 hores, encara que l’artefacte hagi canviat d’estat després. Per reintentar a consciència un error reintentable, envia render una altra vegada amb una clau nova.

La mateixa clau continua en execució

Algunes operacions protegeixen la clau mentre s’executen, per exemple crear un contacte o actualitzar-ne el perfil de client. Si arriba una segona petició amb la mateixa clau abans que acabi la primera, no s’executa: l’API respon 409 idempotency_key_in_use (error.type: idempotency_error) amb param: Idempotency-Key, perquè el resultat encara no es coneix. Espera un moment i envia la mateixa petició una altra vegada amb la mateixa clau: rebràs el resultat desat quan la primera acabi. No canviïs la clau, o executaries l’operació dues vegades. No ho confonguis amb idempotency_key_reused, on la clau ja pertany a un cos diferent.

Exemple de reintent segur

import os, uuid, requests
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

COMPANY_ID = os.environ['COMPANY_ID']

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(
            f'https://api.factuarea.com/v1/companies/{COMPANY_ID}/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();
  const COMPANY_ID = process.env.COMPANY_ID;
  let lastError;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const res = await fetch(`https://api.factuarea.com/v1/companies/${COMPANY_ID}/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.

Les tools de PDF (preview_pdf_template, prepare_pdf_artifact, render_pdf_artifact i upload_pdf_template_asset) i les tools d’escriptura de la biblioteca accepten el mateix argument idempotency_key i el repeteixen abans de la comprovació de versió, igual que les operacions REST.

En aquesta pàgina

Et donem un cop de mà?Contactar amb suport