Skip to main content
Cancelling a CFDI is not a delete — it is a SAT-mediated request that may require the receptor’s acceptance and always produces an acuse de cancelación. This API models it as an async lifecycle on the invoice.

Request

curl -s -X POST localhost:3000/v1/invoices/$INV_ID/cancel \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d '{ "reason": "02" }' | jq '.cancellation'
Response is the invoice with an updated cancellation object:
{ "status": "cancel_pending", "reason": "02",
  "substitute_invoice_id": null, "acuse_available": false }

Motivos de cancelación (SAT)

reasonMeaningSubstitute UUID?
01Comprobante emitido con errores con relación — there is a replacement CFDI.Required — pass substitute_invoice_id (the replacement invoice).
02Comprobante emitido con errores sin relación — no replacement.No.
03No se llevó a cabo la operación.No.
04Operación nominativa relacionada en una factura global.No.
For 01, first stamp the corrected invoice, then cancel the original referencing it:
curl -s -X POST localhost:3000/v1/invoices/$OLD_ID/cancel \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d "{ \"reason\": \"01\", \"substitute_invoice_id\": \"$NEW_ID\" }" | jq '.cancellation'
Sending a substitute for 02/03/04 (or omitting it for 01) is rejected by the SAT — see the cancel.substitution_folio_* and cancel.motivo_* codes in ../errors.md.

The cancellation lifecycle

none ──cancel──▶ cancel_pending ──receptor accepts / 72h lapse──▶ cancelled

                        └── receptor rejects ─────────────────────▶ cancel_rejected
  • cancelled immediately when the CFDI is cancelable sin aceptación — e.g. low-amount invoices, or a receptor of XAXX010101000 (público en general). The SAT accepts on submission.
  • cancel_pending when the CFDI is cancelable con aceptación. The receptor has 72 business hours to accept or reject in their SAT buzón; if they do nothing it auto-cancels at the deadline. The API polls consultarEstatusComprobante on a job and finalizes the status.
  • cancel_rejected when the receptor rejects, or the SAT returns a definitive negative verdict (e.g. the UUID is referenced by another live CFDI → cancel.not_applicable / cancel.not_cancellable).

The receptor-acceptance flow (con aceptación)

  1. You submit the cancellation → cancel_pending.
  2. The receptor sees a solicitud de cancelación in their SAT portal and accepts or rejects it.
  3. The worker’s cancellation-poll job re-queries SAT status until it resolves, then updates the invoice and emits a webhook: invoice.cancelled or invoice.cancel_rejected.
Subscribe to those events instead of polling yourself:
curl -s -X POST localhost:3000/v1/webhook_endpoints \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d '{ "url": "https://you.example/hook", "enabled_events": ["invoice.cancelled","invoice.cancel_rejected"] }'

The acuse

Once status is cancelled, cancellation.acuse_available becomes true. The acuse is the SAT’s XML receipt of the cancellation — retain it as proof for your accounting.

Notes

  • Only a stamped invoice can be cancelled; drafts have no UUID at the SAT.
  • Cancellation is signed with the emitter’s CSD (handled server-side from the fiscal profile).
  • Time-window rules apply: cancellations past the period’s declaration date, or past the global-invoice deadline, are refused (cancel.date_after_declaration, cancel.global_invoice_deadline).
Under the simulated PAC, whether a cancellation resolves immediately or stays pending depends on the receptor RFC (a público-en-general receptor cancels sin aceptación); pending cancellations are advanced by the poll job when a shared PAC is wired (see the integration notes).