Skip to main content
Un timeout en 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 header Retry-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: cualquier 2xx, 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 omites idempotency_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.
No reintentes con una llave distinta, ni con otro folio. Un timeout de PAC no es evidencia de que nada se timbró — puede que el CFDI ya exista en el SAT y tu reintento con datos nuevos produzca un segundo documento fiscal real, cancelable sólo con la cooperación del receptor.
Lo que sí debes hacer:
  1. 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.timbrado para una factura y pago.timbrado para un complemento de pago. Ver Webhooks.
  2. O consulta el estado tú mismo, sin esperar el webhook: GET /cfdi?serie={serie}&folio={folio}&rfc_emisor={rfc_emisor} o, si ya tienes el id, GET /cfdi/{id}.
  3. O reintenta con la misma idempotency_key. Mientras la reconciliación sigue pendiente, un reintento con la misma llave responde 409 DUPLICATE_IN_PROGRESS (nunca un segundo timbrado). En cuanto la reconciliación confirma el resultado, ese mismo reintento pasa a recibir el 201 original, con Idempotency-Replayed: true. En POST /cfdi/complemento-pago lo que se replaya es la respuesta del complemento, con sus documentos_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 de idempotency_key de arriba, nunca asumas que la operación falló.