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

# Cancelación de CFDI

> Cómo identificar el CFDI a cancelar, incluido uno timbrado con otro proveedor, y qué valor lleva uuid_sustitucion (folio fiscal o id).

`POST /cfdi/cancelar` acepta **exactamente uno** de dos identificadores:

| Campo          | Qué valor lleva                                                                             |
| -------------- | ------------------------------------------------------------------------------------------- |
| `id`           | El identificador interno que te devolvimos al timbrar.                                      |
| `folio_fiscal` | El folio fiscal (UUID del SAT) del comprobante, **incluido uno que timbró otro proveedor**. |

Nunca los dos, nunca ninguno: ambos son UUID y nada en su forma los distingue, así que
adivinar significaría cancelar el documento equivocado de vez en cuando. Enviar los dos, o
ninguno, es `400 VALIDATION_ERROR`.

`motivo` es una de las claves del SAT: `01` (emitido con errores **con** relación),
`02` (emitido con errores **sin** relación), `03` (no se llevó a cabo la operación) y
`04` (operación nominativa relacionada en una factura global).

<Note>
  Un CFDI de otra cuenta responde `404 CFDI_NOT_FOUND`, la misma respuesta que un `id` que
  nunca se emitió. Vale para cancelar y para las cinco rutas de consulta.
</Note>

## `uuid_sustitucion`

Sólo el `motivo` `01` lo usa: es el CFDI que **sustituye** al que estás cancelando. Si
cancelas con `motivo=01` y lo omites, la respuesta es `400 UUID_SUSTITUCION_REQUIRED`.

<Note>
  **Puedes enviar el folio fiscal del CFDI sustituto o su `id` de Ipsofactura.** Con
  `motivo=01` la API resuelve `uuid_sustitucion` primero como folio fiscal (el UUID del
  `TimbreFiscalDigital`, como lo define el SAT) y, si no corresponde a ninguno de tus CFDI, como
  `id` interno (el mismo que usarías en `GET /cfdi/{id}`). En ambos casos el documento tiene que
  estar en tu cuenta; si no, responde `404 CFDI_NOT_FOUND`. El sustituto no puede ser el mismo
  CFDI que cancelas ni uno ya cancelado: ambos casos responden `400 VALIDATION_ERROR`.
</Note>

```json Cancelación con sustitución theme={null}
{
  "id": "9a4f07ff-3abc-435f-b7a6-88c2bc65b4c1",
  "rfc_emisor": "EKU9003173C9",
  "motivo": "01",
  "uuid_sustitucion": "0bcfae68-1f2c-4a51-9c33-5f2ad1a9b7e4"
}
```

Es el mismo identificador que usas en `cfdi_relacionados` al timbrar (folio fiscal), así que
puedes reutilizar el `uuid` que te devolvió el timbrado del sustituto sin traducirlo.

### Qué se valida y qué no

| Situación del CFDI sustituto                                                          | Respuesta                                                                            |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| El valor no tiene formato UUID 8-4-4-4-12                                             | `400 INVALID_UUID_FORMAT`                                                            |
| No existe ningún CFDI con ese `id` (incluye el caso de haber enviado el folio fiscal) | `404 CFDI_NOT_FOUND`                                                                 |
| Existe, pero no está vigente ante el SAT (por ejemplo ya está cancelado)              | La API **no** lo valida: la cancelación se envía al PAC y el rechazo lo emite el SAT |

Un CFDI sustituto sin timbrar no es un caso posible: un CFDI sólo se registra en tu cuenta
después de que el PAC lo timbra, así que todo `id` que exista tiene folio fiscal.

<Note>
  El CFDI sustituto debe ser tuyo y del **mismo emisor** que el CFDI cancelado. Hoy la API no
  verifica esa correspondencia sobre el sustituto (sí sobre el CFDI cancelado, con
  `400 RFC_MISMATCH` si `rfc_emisor` no coincide): si envías el `id` equivocado, la
  inconsistencia la detecta el SAT y la cancelación se rechaza allí.
</Note>

### Refacturación: la cadena completa

Cancelar con `motivo=01` es el segundo paso de una refacturación, no el primero:

1. **Timbra el CFDI correcto** con `POST /cfdi/timbrar`, declarando el CFDI que va a
   sustituir en `cfdi_relacionados` con `tipo_relacion` `04` (*Sustitución de los CFDI
   previos*). Ahí los UUIDs son **folios fiscales**.
2. **Cancela el CFDI original** con `POST /cfdi/cancelar`, `motivo=01` y
   `uuid_sustitucion` = el `uuid` (folio fiscal) o el `id` que te devolvió el paso 1.

```json Paso 1 — el sustituto declara la relación 04 theme={null}
{
  "rfc_emisor": "EKU9003173C9",
  "cfdi_relacionados": [
    {
      "tipo_relacion": "04",
      "uuids": ["B0440BB7-1F2C-4A51-9C33-5F2AD1A9B7E4"]
    }
  ]
}
```

En `cfdi_relacionados` sí puedes relacionar un folio fiscal que no timbramos nosotros (uno de
tu proveedor anterior, por ejemplo): la API lo acepta. `uuid_sustitucion` no: el sustituto
tiene que ser un CFDI de tu cuenta, salvo cuando cancelas un comprobante externo (abajo).

Para el caso de una factura global, ver
[Cancelación de una global con ventas ya facturadas](/factura-global#cancelación-de-una-global-con-ventas-ya-facturadas).

## Cancelar un CFDI timbrado con otro proveedor

Si migraste a Ipsofactura con CFDI vigentes que timbró tu proveedor anterior, puedes
cancelarlos aquí: manda su `folio_fiscal` en lugar de `id`. No hace falta el XML original, ni
el total, ni el receptor — el SAT sólo necesita el UUID, el RFC emisor y el motivo.

Si el folio fiscal resulta ser de un CFDI que **sí** timbramos nosotros, se resuelve a ese
comprobante y su cancelación es la de siempre: no se crea un registro aparte.

```json Cancelar por folio fiscal theme={null}
{
  "folio_fiscal": "B0440BB7-1F2C-4A51-9C33-5F2AD1A9B7E4",
  "rfc_emisor": "EKU9003173C9",
  "rfc_receptor": "XAXX010101000",
  "motivo": "02"
}
```

### Precondición: la empresa y su CSD

`rfc_emisor` debe ser una empresa **activa** de tu cuenta y **con CSD vigente**. Ese
certificado es el que firma la solicitud de cancelación, así que sin él la petición no se
puede ni formular. Si no se cumple, la respuesta es
`400 EXTERNAL_CANCELLATION_NOT_ELIGIBLE`.

Es también lo que acota la ruta a tu cuenta: su único otro dato es un UUID que cualquiera
podría citar.

### `rfc_receptor`

Opcional, y sólo se toma en cuenta al cancelar por `folio_fiscal`: es el único dato del
comprobante ajeno que podemos registrar, y queda en la fila para que aparezca en el webhook y
en la consulta. Al cancelar por `id` se ignora — ahí el receptor que ya tenemos en el
comprobante manda.

En el [sandbox](/sandbox#rfcs-mágicos) es además lo que selecciona un escenario forzado: los
mismos RFC reservados del timbrado, por ejemplo el que deja la cancelación `en_proceso` para
que puedas ejercitar el webhook del desenlace.

### `uuid_sustitucion` de un comprobante ajeno

Con `motivo=01`, `uuid_sustitucion` se resuelve primero contra tus CFDI (folio fiscal o `id`).
Cuando estás cancelando un CFDI **externo** y ese valor no corresponde a ninguno, se toma
tal cual como folio fiscal: el sustituto de un comprobante que timbró el proveedor anterior
suele ser otro de los suyos. Cancelando un CFDI **nuestro**, un identificador desconocido sigue siendo
`404 CFDI_NOT_FOUND`.

## Qué queda registrado de un CFDI externo

Cancelar por `folio_fiscal` deja un registro en tu cuenta para sostener el trámite: el acuse,
la reconciliación de una cancelación `en_proceso`, `GET /cfdi/{id}/estatus` y los webhooks
funcionan exactamente igual que con un CFDI que timbramos nosotros.

Ese registro se declara como tal. `GET /cfdi/{id}` devuelve un campo `origen`:

| Valor     | Significado                                                          |
| --------- | -------------------------------------------------------------------- |
| `json`    | CFDI que timbramos desde un request JSON.                            |
| `xml`     | CFDI que timbramos desde el XML que enviaste.                        |
| `externo` | CFDI que timbró otro proveedor, registrado sólo para su cancelación. |

Una fila `externo` trae **únicamente** el folio fiscal, el emisor y el rastro de la
cancelación. No hay conceptos, totales, receptor completo, sello, XML ni PDF: nunca vimos el
comprobante, y rellenar esos campos con `0.00` o con un RFC genérico sería inventar datos
fiscales. En consecuencia:

* se **excluye** de `GET /cfdi`, para que un listado no mezcle comprobantes con registros;
* `GET /cfdi/{id}/xml` responde `404 DOCUMENT_NOT_AVAILABLE`;
* `GET /cfdi/{id}/pdf` responde `400 VALIDATION_ERROR`: sin sello del SAT no hay
  representación impresa que emitir;
* `GET /cfdi/{id}/acuse` y `GET /cfdi/{id}/estatus` sí responden.

```json GET /cfdi/{id} de un CFDI externo (extracto) theme={null}
{
  "id": "4d0b91f2-6c8a-4e33-9f10-2b7d5a1c8e40",
  "uuid": "B0440BB7-1F2C-4A51-9C33-5F2AD1A9B7E4",
  "origen": "externo",
  "emisor_rfc": "EKU9003173C9",
  "receptor_rfc": "XAXX010101000",
  "estatus": "cancelado",
  "motivo_cancelacion": "02",
  "cancelled_at": "2026-09-03T18:22:41Z"
}
```

## Consultar un CFDI por su folio fiscal

`GET /cfdi/{id}` y sus cuatro rutas hermanas —`/xml`, `/pdf`, `/estatus`, `/acuse`— aceptan en
el path **cualquiera de los dos** identificadores: nuestro `id` interno o el folio fiscal del
SAT. Se prueba primero el `id`; si no resuelve a nada de tu cuenta, el mismo valor se reintenta
como folio fiscal. Así, el UUID que te dio el SAT sirve para consultar sin tener que buscarlo
antes en `GET /cfdi`.

`GET /cfdi/{id}/estatus` devuelve en su campo `id` el identificador **interno** del CFDI, no lo
que pusiste en el path.

<Note>
  `POST /cfdi/cancelar` es la excepción: ahí sigues declarando cuál de los dos envías, porque
  los trata distinto — un `folio_fiscal` desconocido registra un CFDI externo, y un `id`
  desconocido es `404`.
</Note>
