FISCALISTO / API
Idempotencia
Repita cualquier solicitud sin miedo: con la misma Idempotency-Key nada se hace dos veces, ni se timbra, ni se crea, ni se manda un correo.
Una red que se corta a la mitad deja a su sistema sin saber si la factura se timbró. Por eso todo lo que timbra exige el encabezado Idempotency-Key: si repite la solicitud con la misma llave, Fiscalisto le contesta el comprobante que ya timbró en vez de timbrar otro.
curl https://api.fiscalisto.com/v1/invoices \
-H "Authorization: Bearer $FISCALISTO_API_KEY" \
-H "Idempotency-Key: pedido-A-1042" \
-H "Content-Type: application/json" \
-d '{ "customer": "cus_OufpKqnrvx9p1", "payment_form": "04", "items": [{ "product": "prd_OufpKqnrvx9p1" }] }'Dónde se usa
Todo POST de la API acepta Idempotency-Key, como en Stripe o Resend:
- Lo que timbra la exige: facturas, sustituciones, complementos de pago, notas de crédito, anticipos y su aplicación, recibos de nómina, constancias de retenciones, lotes y facturar una cotización. Sin ella responden
idempotency_key_missing. - Lo demás la acepta: crear un cliente, un producto, un webhook, una cotización, una factura recurrente, un ticket, una descarga del SAT, mandar un correo, conectar una tienda… Con la llave, un reintento contesta lo mismo que la primera vez, con
idempotent-replayed: true, y no crea nada otra vez ni manda otro correo.
GET y DELETE no la necesitan: repetirlos no cambia nada.
Cómo elegirla
- De 1 a 255 caracteres ASCII, sin acentos ni ñ (8 o más en lo que timbra). Si no es así,
400idempotency_key_invalid. - Una por comprobante, tomada de algo que su sistema ya tiene y que no cambia al reintentar: el id del pedido, del cobro o de la fila en su base de datos (
pedido-A-1042,pago-7781). Si no tiene uno, genere un UUID al crear el pedido y guárdelo con él. - Nunca la genere en el momento de llamar: un reintento tendría otra llave y timbraría dos veces.
Qué contesta al repetirla
| Situación | Respuesta |
|---|---|
| La primera vez | 201 con el comprobante timbrado (o 202 si el PAC aún no contesta). |
| Repetida, ya timbrado | 201 (lo mismo que la primera vez) con el mismo comprobante y el encabezado idempotent-replayed: true. No usa otro timbre. |
| Repetida mientras la primera sigue timbrando | 409 request_in_progress con Retry-After. Espere y repítala. |
| Repetida con otro cuerpo | 409 idempotency_key_reused. No se timbra nada. |
La primera no pasó la revisión (422 invoice_invalid) | La llave queda libre: mande el cuerpo corregido con la misma llave. |
La primera la rechazó el SAT (422 invoice_rejected) o no había timbres (402) | Repetirla con el mismo cuerpo lo intenta de nuevo. Con otro cuerpo, 409 idempotency_key_reused. |
La llave guarda el cuerpo con el que se usó desde que llegó al PAC. Si el SAT rechazó una factura y la corrige, el cuerpo es otro: use una llave nueva (pedido-A-1042-2). Repetir con la misma llave y el mismo cuerpo sirve cuando el problema no estaba en los datos: el PAC no respondió, se acabaron los timbres y ya compró, o el emisor aún no tenía su CSD. Un invoice_invalid nunca llegó al PAC, así que ahí sí puede corregir y repetir con la misma llave.
Varias solicitudes con la misma llave al mismo tiempo (un doble clic, dos servidores) timbran una sola vez: una recibe el comprobante y las demás 409 request_in_progress hasta que termine; repetidas, reciben el mismo comprobante.
Cuánto dura una llave
Una llave es de su cuenta y de su modo: la misma en pruebas y en producción, o en dos entornos de prueba, son llaves distintas.
- En todo
POSTla llave queda ligada a esa ruta y ese cuerpo 24 horas: usarla para otra cosa responde409idempotency_key_reused, aunque sea otra ruta. - Solo se guardan las respuestas correctas (
2xx). Si la primera vez respondió un error, no se hizo nada y la llave queda libre: corrija y repita con la misma llave. - Lo que timbra guarda además el comprobante de cada llave para siempre: una llave que ya timbró siempre contesta ese comprobante, aunque pasen meses.
Después de 24 horas, la llave de un POST que no timbra vuelve a estar libre.
Reintentos recomendados
Los SDKs (TypeScript, Python, PHP, .NET, Go y Java) ya hacen todo esto en cada POST: ponen una Idempotency-Key si usted no la da, la conservan en cada reintento y esperan lo que diga Retry-After. Así ningún reintento del SDK crea o manda algo dos veces. Lo siguiente es para llamar al API directo.
Reintente con la misma llave cuando:
- no llegó respuesta (se cortó la conexión o se acabó el tiempo de espera);
- llegó
409request_in_progress,429o un5xx, esperando lo que digaRetry-Aftero, si no viene, con espera creciente (1, 2, 4, 8 segundos).
No reintente un 400, 402, 403, 404 o 422 sin cambiar algo: la respuesta sería la misma.
async function timbrar(body, idempotencyKey) {
for (let intento = 0; intento < 5; intento++) {
let res;
try {
res = await fetch("https://api.fiscalisto.com/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FISCALISTO_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
} catch {
// Sin respuesta: repetir con la misma llave nunca timbra dos veces.
}
if (res && res.status !== 409 && res.status !== 429 && res.status < 500) return res.json();
const espera = Number(res?.headers.get("retry-after")) || 2 ** intento;
await new Promise((r) => setTimeout(r, espera * 1000));
}
throw new Error("Fiscalisto no respondió; reintente más tarde con la misma Idempotency-Key.");
}409 idempotency_key_reused no se reintenta
El reintento del ejemplo vuelve a mandar el mismo cuerpo, así que un 409 es siempre request_in_progress. Si su código puede cambiar el cuerpo entre intentos, revise error.code antes de esperar.
Autenticación y llaves
Cómo se identifica su sistema con su llave secreta, cómo limitarla a ciertos emisores, a solo lectura o a un plazo, y cómo cambiarla sin cortes.
Versiones
Cada cuenta usa una versión de la API, por fecha. Un cambio que no es compatible llega como una versión nueva, y usted decide cuándo moverse.