POST /cfdi/timbrar, /cfdi/complemento-pago o /cfdi/cancelar deja una
ambigüedad: la operación pudo haberse completado (y la respuesta se perdió en el camino) o
no. Esta guía documenta el patrón seguro: idempotency_key.
idempotency_key
Envía un campo idempotency_key en el body de cualquiera de los endpoints de escritura:
POST /cfdi/timbrar, /cfdi/timbrar-xml, /cfdi/timbrar-sellado, /cfdi/complemento-pago
y /cfdi/cancelar. Un reintento con la misma llave te devuelve la respuesta del primer
intento tal cual, byte a byte (mismo status, mismo cuerpo, header
Idempotency-Replayed: true) en lugar de volver a ejecutarse. La misma llave con un cuerpo
distinto responde 422 IDEMPOTENCY_KEY_REUSED y no ejecuta nada.
- Máximo 100 caracteres, ASCII imprimibles y sin espacios (la llave viaja también como identificador interno hacia el PAC).
- Se recomienda algo determinista de tu lado: por ejemplo
"{tu_id_de_operacion}"o"{rfc_emisor}-{serie}-{folio}".
Cabecera Idempotency-Key
Si tu cliente HTTP o tu gateway ya manejan la convención de la cabecera Idempotency-Key,
puedes enviarla en lugar del campo del body: tiene exactamente el mismo efecto, las mismas
reglas de formato y la respuesta te devuelve la llave en idempotency_key. Si envías ambos
con valores distintos, la petición responde 400 VALIDATION_ERROR antes de ejecutar nada.
cURL
cURL
1
Genera una idempotency_key por operación
Antes de llamar al endpoint, decide la llave (tu id de operación interno es la opción
más simple).
2
Ante timeout, reenvía la misma request con la misma llave
Hay salidas deterministas:
- 2xx — replay del resultado exitoso del primer intento, o el CFDI se timbra ahora si el primer intento nunca llegó a ejecutarse.
- 409
DUPLICATE_IN_PROGRESS— el primer intento sigue procesándose. Trae un headerRetry-After; espera esos segundos y reintenta con la misma llave. - 422
IDEMPOTENCY_KEY_REUSED— reutilizaste la llave con un payload distinto del primer intento. Usa una llave nueva para una operación nueva, o reenvía el payload original si quieres su respuesta.
Qué se replaya y qué no
Un reintento con la misma llave devuelve la respuesta guardada cuando el primer intento terminó en un resultado determinista: cualquier2xx, y los 4xx que dependen del
payload (400, 404, 409, 422).
No se replayan los códigos que son un veredicto sobre ti como llamante y no sobre el
contenido de la request — 401, 402 (tu límite de prueba se puede subir), 403 (tu
cuenta se puede reactivar), 408, 429 — ni ningún 5xx. En esos casos la reserva se
libera y tu reintento se ejecuta de verdad.
Llave derivada, si no envías una
Si omitesidempotency_key pero tu request trae serie y folio propios, Ipsofactura
deriva una llave internamente a partir de RFC emisor + tipo de comprobante + serie + folio,
así que el reintento sigue deduplicando. Si además omites el folio (dejas que Ipsofactura lo
asigne), no hay nada estable de qué derivar la llave — cada envío es un CFDI nuevo. Para
reintentos seguros sin depender de esto, asigna tu propia idempotency_key explícita.
Retención
Una reserva de idempotencia vive 96 horas. Pasado ese plazo, reutilizar la misma llave ya no deduplica — es una llave libre otra vez.El caso especial: 503 STAMPING_OUTCOME_UNKNOWN
Cuando el PAC no responde a tiempo, POST /cfdi/timbrar (o /cfdi/complemento-pago) puede
devolver 503 con código STAMPING_OUTCOME_UNKNOWN. A diferencia de un 503 genérico, este
significa algo preciso: el comprobante pudo haberse timbrado en el SAT sin que nosotros lo
sepamos todavía, y Ipsofactura ya está resolviendo la ambigüedad por ti.
Lo que sí debes hacer:
- Espera el webhook del timbrado. Ipsofactura vuelve a preguntarle al PAC en
segundo plano (backoff creciente, hasta 72 horas) y, en cuanto confirma el resultado,
entrega el webhook — idéntico al de un timbrado a tiempo:
cfdi.timbradopara una factura ypago.timbradopara un complemento de pago. Ver Webhooks. - O consulta el estado tú mismo, sin esperar el webhook:
GET /cfdi?serie={serie}&folio={folio}&rfc_emisor={rfc_emisor}o, si ya tienes elid,GET /cfdi/{id}. - O reintenta con la misma
idempotency_key. Mientras la reconciliación sigue pendiente, un reintento con la misma llave responde409 DUPLICATE_IN_PROGRESS(nunca un segundo timbrado). En cuanto la reconciliación confirma el resultado, ese mismo reintento pasa a recibir el201original, conIdempotency-Replayed: true. EnPOST /cfdi/complemento-pagolo que se replaya es la respuesta del complemento, con susdocumentos_relacionados— no la de una factura.
El complemento de pago (
POST /cfdi/complemento-pago) recorre exactamente este mismo camino:
el intento queda registrado, la reconciliación persiste el árbol de pagos y documentos
relacionados, y el desenlace llega como pago.timbrado tardío. Importa más que en una
factura, porque la cadena de parcialidades de la PPD se deriva de los pagos registrados. Ver
Complemento de pago.Si la reconciliación agota sus reintentos sin que el PAC confirme nada (caso raro), el
CFDI queda sin resolver y tu
idempotency_key sigue tomada hasta que expire la retención
de 96 h. Contacta a soporte con el idempotency_key si esto te ocurre.Timeouts recomendados
El timbrado normalmente responde en menos de 3 segundos (incluyendo el failover entre PACs). Configura en tu cliente un timeout de 30 segundos para los endpoints de escritura y trata cualquier corte como ambiguo: aplica el patrón deidempotency_key de
arriba, nunca asumas que la operación falló.