Cancelar Factura
Solicita la cancelacion de una factura timbrada ante el SAT.
La cancelacion no es inmediata. El SAT puede requerir aceptacion del receptor si la factura es mayor a $5,000 MXN y tiene mas de 24 horas de emitida.
Endpoint
POST https://api.lummy.io/v1/invoices/{invoiceId}/cancel
| Entorno | URL |
|---|---|
| Producción | https://api.lummy.io/v1/invoices/{invoiceId}/cancel |
| Sandbox | https://sandbox.lummy.io/v1/invoices/{invoiceId}/cancel |
Headers
Path Parameters
Body Parameters
Motivos de Cancelacion
| Clave | Descripcion | Requiere Sustituto |
|---|---|---|
01 | Comprobante emitido con errores con relacion | Si |
02 | Comprobante emitido con errores sin relacion | No |
03 | No se llevo a cabo la operacion | No |
04 | Operacion nominativa relacionada en factura global | No |
Ejemplos de Codigo
- cURL
- Node.js (TypeScript)
- Python
- PHP (Guzzle)
# Cancelacion por error sin relacion (motivo 02)
curl -X POST https://sandbox.lummy.io/invoices/550e8400-e29b-41d4-a716-446655440000/cancel \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "x-organization-id: ${LUMMY_ORG_ID}" \
-H "x-api-key: ${LUMMY_API_KEY}" \
-H "x-idempotency: $(uuidgen)" \
-d '{
"motivo": "02"
}'
# Cancelacion por error con relacion (motivo 01) - requiere factura sustituta
curl -X POST https://sandbox.lummy.io/invoices/550e8400-e29b-41d4-a716-446655440000/cancel \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "x-organization-id: ${LUMMY_ORG_ID}" \
-H "x-api-key: ${LUMMY_API_KEY}" \
-H "x-idempotency: $(uuidgen)" \
-d '{
"motivo": "01",
"folioSustituto": "cfa52b8b-93f2-4e6b-8c73-64ad88deb17c"
}'
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
interface CancelInvoicePayload {
motivo: '01' | '02' | '03' | '04';
folioSustituto?: string;
}
interface CancelInvoiceResponse {
invoiceId: string;
cfdiUuid: string;
status: 'PENDING_CANCELLATION' | 'CANCELLED';
cancellationRequestedAt: string;
cancellationAcknowledgement?: string;
}
async function cancelarFactura(
invoiceId: string,
motivo: '01' | '02' | '03' | '04',
folioSustituto?: string
): Promise<CancelInvoiceResponse> {
const API_URL = `https://sandbox.lummy.io/invoices/${invoiceId}/cancel`;
const payload: CancelInvoicePayload = { motivo };
if (motivo === '01' && folioSustituto) {
payload.folioSustituto = folioSustituto;
}
const response = await axios.post<CancelInvoiceResponse>(API_URL, payload, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ACCESS_TOKEN}`,
'x-organization-id': process.env.LUMMY_ORG_ID!,
'x-api-key': process.env.LUMMY_API_KEY!,
'x-idempotency': uuidv4(),
},
});
console.log('Solicitud de cancelacion enviada');
console.log('Estado:', response.data.status);
return response.data;
}
// Ejemplo: Cancelar por error sin relacion
cancelarFactura('550e8400-e29b-41d4-a716-446655440000', '02');
import os
import requests
from uuid import uuid4
def cancelar_factura(invoice_id, motivo, folio_sustituto=None):
"""
Cancela una factura ante el SAT.
Args:
invoice_id: UUID de la factura
motivo: '01', '02', '03', o '04'
folio_sustituto: UUID de factura sustituta (solo para motivo '01')
"""
api_url = f"https://sandbox.lummy.io/invoices/{invoice_id}/cancel"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.getenv('ACCESS_TOKEN')}",
"x-organization-id": os.getenv("LUMMY_ORG_ID"),
"x-api-key": os.getenv("LUMMY_API_KEY"),
"x-idempotency": str(uuid4()),
}
payload = {"motivo": motivo}
if motivo == "01" and folio_sustituto:
payload["folioSustituto"] = folio_sustituto
response = requests.post(api_url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
print(f"Solicitud de cancelacion enviada")
print(f"Estado: {data['status']}")
return data
if __name__ == "__main__":
# Cancelar por error sin relacion
cancelar_factura("550e8400-e29b-41d4-a716-446655440000", "02")
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use Ramsey\Uuid\Uuid;
function cancelarFactura(
string $invoiceId,
string $motivo,
?string $folioSustituto = null
): array {
$client = new Client([
'base_uri' => 'https://sandbox.lummy.io',
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . getenv('ACCESS_TOKEN'),
'x-organization-id' => getenv('LUMMY_ORG_ID'),
'x-api-key' => getenv('LUMMY_API_KEY'),
'x-idempotency' => Uuid::uuid4()->toString(),
],
]);
$payload = ['motivo' => $motivo];
if ($motivo === '01' && $folioSustituto) {
$payload['folioSustituto'] = $folioSustituto;
}
$response = $client->post("/invoices/{$invoiceId}/cancel", [
'json' => $payload,
]);
$data = json_decode($response->getBody(), true);
echo "Solicitud de cancelacion enviada\n";
echo "Estado: " . $data['status'] . "\n";
return $data;
}
// Cancelar por error sin relacion
cancelarFactura('550e8400-e29b-41d4-a716-446655440000', '02');
Respuestas
Todas las respuestas siguen el formato estándar StandardResponse.
200 OK - Cancelacion Inmediata
Cuando la cancelacion es procesada inmediatamente (facturas menores a $5,000 o dentro de 24 horas).
{
"requestId": "abc123-def456",
"data": {
"invoiceId": "550e8400-e29b-41d4-a716-446655440000",
"cfdiUuid": "cfa52b8b-93f2-4e6b-8c73-64ad88deb17c",
"status": "CANCELLED",
"cancellationRequestedAt": "2025-01-20T10:30:00.000Z",
"cancellationAcknowledgement": "ABC123..."
},
"timestamp": "2025-01-20T10:30:00.000Z",
"path": "/invoices/550e8400-e29b-41d4-a716-446655440000/cancel",
"method": "POST"
}
202 Accepted - Pendiente de Aceptacion
Cuando el receptor debe aceptar la cancelacion (facturas mayores a $5,000 y con mas de 24 horas).
{
"requestId": "abc123-def456",
"data": {
"invoiceId": "550e8400-e29b-41d4-a716-446655440000",
"cfdiUuid": "cfa52b8b-93f2-4e6b-8c73-64ad88deb17c",
"status": "PENDING_CANCELLATION",
"cancellationRequestedAt": "2025-01-20T10:30:00.000Z",
"message": "La cancelacion requiere aceptacion del receptor. El receptor tiene 72 horas para responder."
},
"timestamp": "2025-01-20T10:30:00.000Z",
"path": "/invoices/550e8400-e29b-41d4-a716-446655440000/cancel",
"method": "POST"
}
El receptor tiene 72 horas para aceptar o rechazar la cancelacion. Si no responde, la cancelacion se aprueba automaticamente.
400 Bad Request
Motivo invalido o falta folio sustituto para motivo 01.
{
"requestId": "abc123-def456",
"error": {
"message": "El motivo '01' requiere el campo folioSustituto",
"code": "ValidationError",
"status": 400
},
"timestamp": "2025-01-20T10:30:00.000Z",
"path": "/invoices/550e8400-e29b-41d4-a716-446655440000/cancel",
"method": "POST"
}
404 Not Found
Factura no encontrada.
{
"requestId": "abc123-def456",
"error": {
"message": "Invoice not found",
"code": "NotFoundException",
"status": 404
},
"timestamp": "2025-01-20T10:30:00.000Z",
"path": "/invoices/550e8400-e29b-41d4-a716-446655440000/cancel",
"method": "POST"
}
409 Conflict
La factura ya esta cancelada o no se puede cancelar.
{
"requestId": "abc123-def456",
"error": {
"message": "La factura ya se encuentra cancelada",
"code": "ConflictException",
"status": 409
},
"timestamp": "2025-01-20T10:30:00.000Z",
"path": "/invoices/550e8400-e29b-41d4-a716-446655440000/cancel",
"method": "POST"
}
422 Unprocessable Entity
El SAT rechazo la cancelacion.
{
"requestId": "abc123-def456",
"error": {
"message": "El SAT rechazo la cancelacion: El receptor no acepto la solicitud",
"code": "UnprocessableEntityException",
"status": 422
},
"timestamp": "2025-01-20T10:30:00.000Z",
"path": "/invoices/550e8400-e29b-41d4-a716-446655440000/cancel",
"method": "POST"
}
Estados de Cancelacion
| Estado | Descripcion |
|---|---|
PENDING_CANCELLATION | Solicitud enviada, esperando aceptacion del receptor. |
CANCELLED | Factura cancelada exitosamente. |
CANCELLATION_REJECTED | El receptor rechazo la cancelacion. |
Notas Importantes
Reglas del SAT
- Facturas menores a $5,000 MXN: Se cancelan inmediatamente sin necesidad de aceptacion.
- Facturas mayores a $5,000 MXN emitidas hace menos de 24 horas: Se cancelan inmediatamente.
- Facturas mayores a $5,000 MXN emitidas hace mas de 24 horas: Requieren aceptacion del receptor.
Cancelacion con Sustitucion (Motivo 01)
Cuando cancelas una factura por errores y emites una nueva que la sustituye:
- Primero emite la nueva factura correcta
- Luego cancela la factura erronea usando el UUID de la nueva como
folioSustituto
// 1. Emitir factura correcta
const nuevaFactura = await crearFactura(datosCorrectos);
// 2. Cancelar factura erronea
await cancelarFactura(facturaErroneaId, '01', nuevaFactura.cfdiUuid);
Consultar Estado de Cancelacion
Para verificar si una cancelacion pendiente fue aceptada, consulta la factura:
curl -X GET https://sandbox.lummy.io/invoices/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "x-organization-id: ${LUMMY_ORG_ID}"