CFDI cancellations
This guide explains how invoice cancellations work in Facturapi, what each status means, and what to expect after requesting a cancellation.
Requirements and rules
- You can only cancel invoices with
status: "valid". - If the invoice is
draft, the cancel method deletes it. - If the invoice is already
canceledor has a cancellation in progress (cancellation_status: "pending"or"verifying"), the method will return an error.
Cancellation motives (SAT) and substitution
The SAT defines cancellation motives, and Facturapi exposes them via the motive parameter. Cancellations use the SAT's new cancellation scheme (effective since 2022).
01: Invoice issued with errors with relation. Requiressubstitutionwith the UUID (or Facturapi ID) of the replacing invoice.02: Invoice issued with errors without relation.03: Operation not carried out.04: Nominative operation related to a global invoice.
The SAT expects the motive to match the use case; otherwise the cancellation may fail.
Notes:
- If you choose motive
01, you must sendsubstitution, and the replacement invoice must exist. - If the SAT considers your CFDI non-cancellable (by status or relationships), the request will be rejected even if the motive is valid.
Cancellation flow and statuses
To cancel an invoice, use cancelInvoice with a cancellation motive:
- cURL
- Node.js
- Python
- C#
curl "https://www.facturapi.io/v2/invoices/58e93bd8e86eb318b019743d?motive=02" \
-H "Authorization: Bearer sk_test_API_KEY" \
-X DELETE
import Facturapi from 'facturapi';
const facturapi = new Facturapi('sk_test_API_KEY');
const invoice = await facturapi.invoices.cancel(
'58e93bd8e86eb318b019743d',
{ motive: '02' }
);
from facturapi import Facturapi
facturapi = Facturapi('sk_test_API_KEY')
invoice = facturapi.invoices.cancel('58e93bd8e86eb318b019743d', {
'motive': '02'
})
var facturapi = new FacturapiClient("sk_test_API_KEY");
var invoice = await facturapi.Invoice.CancelAsync(
"58e93bd8e86eb318b019743d",
new Dictionary<string, object>
{
["motive"] = "02"
}
);
After calling cancelInvoice, you may get:
- Success with
status: "canceled"andcancellation_status: "accepted"(finalized).
{
"id": "58e93bd8e86eb318b019743d",
"status": "canceled",
"cancellation_status": "accepted",
"uuid": "39c85a3f-275b-4341-b259-e8971d9f8a94"
}
- Success with
status: "valid"andcancellation_status: "pending"(requires receiver approval).
{
"id": "58e93bd8e86eb318b019743d",
"status": "valid",
"cancellation_status": "pending",
"uuid": "39c85a3f-275b-4341-b259-e8971d9f8a94"
}
- Success with
status: "valid"andcancellation_status: "verifying"(SAT received the request and is validating it).
{
"id": "58e93bd8e86eb318b019743d",
"status": "valid",
"cancellation_status": "verifying",
"uuid": "39c85a3f-275b-4341-b259-e8971d9f8a94"
}
- An error explaining why the cancellation could not be completed.
Meaning of cancellation_status:
| Status | Meaning | What to do |
|---|---|---|
none | No request registered by the SAT. | You can request cancellation if needed. |
verifying | SAT received the request and is validating it. | Wait for automatic updates or fetch the invoice. |
pending | Receiver approval is required. | Wait for acceptance/rejection or expiration. |
accepted | Cancellation was accepted. | Invoice moves to status: "canceled". |
rejected | Cancellation was rejected. | Review the motive and retry if applicable. |
expired | Receiver did not respond in time. | You may retry cancellation if applicable. |
Automatic status updates
Facturapi periodically checks the SAT and updates cancellation_status. You can:
- Fetch the invoice using Get Invoice.
- Subscribe to the
invoice.cancellation_status_updatedwebhook to receive changes when the status leavespendingorverifying.
Cancellation receipt and verification
When the cancellation is accepted, you can download the cancellation receipt in XML or PDF from the Cancellation receipt endpoint. You can also validate the CFDI using verification_url.
- cURL
- Node.js
- Python
- C#
# XML receipt
curl "https://www.facturapi.io/v2/invoices/58e93bd8e86eb318b019743d/cancellation_receipt/xml" \
-H "Authorization: Bearer sk_test_API_KEY" \
-X GET \
-o "cancellation_receipt.xml"
# PDF receipt
curl "https://www.facturapi.io/v2/invoices/58e93bd8e86eb318b019743d/cancellation_receipt/pdf" \
-H "Authorization: Bearer sk_test_API_KEY" \
-X GET \
-o "cancellation_receipt.pdf"
import Facturapi from 'facturapi';
import fs from 'node:fs';
const facturapi = new Facturapi('sk_test_API_KEY');
const xmlStream = await facturapi.invoices.downloadCancellationReceiptXml('58e93bd8e86eb318b019743d');
xmlStream.pipe(fs.createWriteStream('cancellation_receipt.xml'));
const pdfStream = await facturapi.invoices.downloadCancellationReceiptPdf('58e93bd8e86eb318b019743d');
pdfStream.pipe(fs.createWriteStream('cancellation_receipt.pdf'));
from facturapi import Facturapi
facturapi = Facturapi('sk_test_API_KEY')
xml_stream = facturapi.invoices.download_cancellation_receipt_xml('58e93bd8e86eb318b019743d')
with open('cancellation_receipt.xml', 'wb') as file:
file.write(xml_stream.read())
pdf_stream = facturapi.invoices.download_cancellation_receipt_pdf('58e93bd8e86eb318b019743d')
with open('cancellation_receipt.pdf', 'wb') as file:
file.write(pdf_stream.read())
var facturapi = new FacturapiClient("sk_test_API_KEY");
var xmlStream = await facturapi.Invoice.DownloadCancellationReceiptXmlAsync("58e93bd8e86eb318b019743d");
var xmlFile = new System.IO.FileStream("C:\\route\\to\\save\\cancellation_receipt.xml", FileMode.Create);
xmlStream.CopyTo(xmlFile);
xmlFile.Close();
var pdfStream = await facturapi.Invoice.DownloadCancellationReceiptPdfAsync("58e93bd8e86eb318b019743d");
var pdfFile = new System.IO.FileStream("C:\\route\\to\\save\\cancellation_receipt.pdf", FileMode.Create);
pdfStream.CopyTo(pdfFile);
pdfFile.Close();
Common errors
409 Conflict: the invoice is notvalidor already has a cancellation in progress.400 Bad Request: invalid motive or missingsubstitutionwhen required by SAT rules.404 Not Found: the folio or UUID does not exist in the SAT.