> ## Documentation Index
> Fetch the complete documentation index at: https://developers.ipsofactura.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recuperación tras timeout

> Qué hacer cuando un timbrado no responde, sin arriesgar un doble timbre.

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.

```bash cURL theme={null}
curl -X POST https://api.ipsofactura.com/cfdi/timbrar-xml \
  -H "x-api: {tu_api_key}" \
  -H "Idempotency-Key: erp-2026-08-11-A-1" \
  -H "Content-Type: application/json" \
  -d '{ "xml_base64": "PD94bWwg..." }'
```

```bash cURL theme={null}
curl -X POST https://api.ipsofactura.com/cfdi/timbrar \
  -H "x-api: {tu_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc_emisor": "EKU9003173C9",
    "idempotency_key": "erp-venta-48213",
    "serie": "A",
    "folio": 1042,
    "...": "resto del comprobante"
  }'
```

<Steps>
  <Step title="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).
  </Step>

  <Step title="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.
  </Step>
</Steps>

### 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.

```json theme={null}
{
  "code": "STAMPING_OUTCOME_UNKNOWN",
  "message": "El resultado del timbrado no se pudo confirmar. Ipsofactura reintentará la consulta automáticamente",
  "retry_after": 60,
  "idempotency_key": "erp-venta-48213"
}
```

| Campo             | Descripción                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| `retry_after`     | Segundos hasta el próximo intento de reconciliación (también en el header `Retry-After`).        |
| `idempotency_key` | Tu propia llave, si la enviaste — para correlacionar el eventual webhook o un ticket de soporte. |

<Warning>
  **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.
</Warning>

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](/webhooks#eventos-de-cancelación-y-timbrado-tardío).
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.

<Note>
  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](/complemento-pago#si-el-timbrado-del-rep-no-responde).
</Note>

<Note>
  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.
</Note>

## 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ó.
