Emitir una Factura (Todo-en-Uno)
Crea y timbra una factura CFDI 4.0 en una sola llamada. Lummy valida los datos, calcula impuestos, sella el CFDI con tu CSD y lo timbra automaticamente con el PAC.
Un CFDI 4.0 timbrado con UUID fiscal, archivos XML y PDF almacenados en S3, listo para entregar a tu cliente.
Si necesitas firmar los CFDI con tu propia infraestructura, usa el flujo de dos pasos:
POST /invoices/prepare- Obtiene XML sin sellar y cadena originalPOST /invoices/stamp- Envia el XML ya sellado para timbrado
Este flujo alternativo no almacena datos en base de datos ni logs, ideal para empresas con requisitos estrictos de privacidad.
Endpoint
POST https://api.lummy.io/v1/invoices
| Entorno | URL |
|---|---|
| Producción | https://api.lummy.io/v1/invoices |
| Sandbox | https://sandbox.lummy.io/v1/invoices |
Autenticación y Headers
*Debes usar uno de los dos métodos de autenticación: Authorization (Bearer JWT) o x-api-key.
El header x-idempotency garantiza que no se dupliquen facturas. Si reenvías el mismo request con el mismo UUID, recibirás la factura original sin crear una nueva. Genera un UUID único por cada factura nueva.
Este endpoint tiene un límite de 30 solicitudes por minuto por organización. Si excedes este límite, recibirás un error 429 Too Many Requests.
Estructura del Request
El cuerpo de la petición es un objeto JSON que representa el CrearComprobanteDto.
Objeto Principal: CrearComprobanteDto
Ejemplos de Código
A continuación, ejemplos funcionales en diferentes lenguajes para integrar el endpoint en tu sistema:
- cURL
- Node.js (TypeScript)
- Python
- PHP (Guzzle)
curl -X POST https://sandbox.lummy.io/invoices \
-H "Content-Type: application/json" \
-H "x-organization-id: ${LUMMY_ORG_ID}" \
-H "x-api-key: ${LUMMY_API_KEY}" \
-H "x-idempotency: $(uuidgen)" \
-d '{
"tipoDeComprobante": "I",
"receptor": {
"rfc": "XAXX010101000",
"nombre": "PUBLICO EN GENERAL",
"regimenFiscal": "616",
"domicilioFiscal": "06500",
"email": "cliente@ejemplo.com"
},
"usoCFDI": "G03",
"metodoPago": "PUE",
"formaPago": "03",
"fecha": "2025-11-19T10:00:00",
"moneda": "MXN",
"exportacion": "01",
"lugarExpedicion": "06500",
"conceptos": [
{
"claveProdServ": "81111500",
"claveUnidad": "E48",
"descripcion": "Desarrollo de Software - Sistema Web a Medida",
"cantidad": 1,
"valorUnitario": 15000.00,
"objetoImp": "02",
"impuestos": {
"traslados": [
{
"base": 15000.00,
"impuesto": "002",
"tipoFactor": "Tasa",
"tasaOCuota": 0.16
}
]
}
}
]
}'
Define LUMMY_ORG_ID y LUMMY_API_KEY en tu shell:
export LUMMY_ORG_ID="your-organization-uuid"
export LUMMY_API_KEY="your-api-key"
import axios, {AxiosError} from 'axios';
import {v4 as uuidv4} from 'uuid';
// Interfaces para type-safety
interface Receptor {
rfc: string;
nombre: string;
regimenFiscal: string;
domicilioFiscal: string;
email: string;
telefono?: string;
}
interface ConceptoTraslado {
base: number;
impuesto: string;
tipoFactor: string;
tasaOCuota: number;
}
interface ConceptoImpuestos {
traslados?: ConceptoTraslado[];
}
interface Concepto {
claveProdServ: string;
claveUnidad: string;
descripcion: string;
cantidad: number;
valorUnitario: number;
objetoImp: string;
impuestos?: ConceptoImpuestos;
}
interface InvoicePayload {
tipoDeComprobante: string;
receptor: Receptor;
usoCFDI: string;
metodoPago: string;
formaPago: string;
fecha: string;
moneda: string;
exportacion: string; // '01' = No aplica exportación
lugarExpedicion: string;
conceptos: Concepto[];
}
interface InvoiceResponse {
invoiceId: string;
cfdiUuid: string;
verificationUrl: string;
xmlS3Url: string;
pdfS3Url: string;
}
async function crearFactura(): Promise<InvoiceResponse> {
const API_URL = 'https://sandbox.lummy.io/invoices';
const ORG_ID = process.env.LUMMY_ORG_ID!;
const API_KEY = process.env.LUMMY_API_KEY!;
const payload: InvoicePayload = {
tipoDeComprobante: 'I', // I = Ingreso
receptor: {
rfc: 'XAXX010101000',
nombre: 'PUBLICO EN GENERAL',
regimenFiscal: '616', // Sin obligaciones fiscales
domicilioFiscal: '06500',
email: 'cliente@ejemplo.com',
},
usoCFDI: 'G03', // Gastos en general
metodoPago: 'PUE', // Pago en Una Exhibición
formaPago: '03', // Transferencia electrónica
fecha: new Date().toISOString().slice(0, 19), // Formato: 2025-11-19T10:00:00
moneda: 'MXN',
exportacion: '01', // No aplica
lugarExpedicion: '06500',
conceptos: [
{
claveProdServ: '81111500', // Servicios de ingeniería de software
claveUnidad: 'E48', // Unidad de servicio
descripcion: 'Desarrollo de Software - Sistema Web a Medida',
cantidad: 1,
valorUnitario: 15000.0,
objetoImp: '02', // Sí objeto de impuesto
impuestos: {
traslados: [
{
base: 15000.0,
impuesto: '002', // IVA
tipoFactor: 'Tasa',
tasaOCuota: 0.16, // 16%
},
],
},
},
],
};
try {
const response = await axios.post<InvoiceResponse>(API_URL, payload, {
headers: {
'Content-Type': 'application/json',
'x-organization-id': ORG_ID,
'x-api-key': API_KEY,
'x-idempotency': uuidv4(), // UUID único por factura
},
});
console.log('Factura timbrada exitosamente');
console.log('UUID Fiscal:', response.data.cfdiUuid);
console.log('Descargar XML:', response.data.xmlS3Url);
console.log('Descargar PDF:', response.data.pdfS3Url);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
console.error('Error al timbrar:', axiosError.response?.data);
throw axiosError;
}
throw error;
}
}
// Ejecutar
crearFactura();
npm install axios uuid
npm install -D @types/uuid
import os
import requests
from uuid import uuid4
from datetime import datetime
def crear_factura():
"""
Crea y timbra una factura de ingreso usando la API de Lummy.
Returns:
dict: Respuesta con invoiceId, cfdiUuid, URLs de descarga
"""
api_url = "https://sandbox.lummy.io/invoices"
org_id = os.getenv("LUMMY_ORG_ID")
api_key = os.getenv("LUMMY_API_KEY")
if not org_id or not api_key:
raise ValueError("Debes definir LUMMY_ORG_ID y LUMMY_API_KEY")
headers = {
"Content-Type": "application/json",
"x-organization-id": org_id,
"x-api-key": api_key,
"x-idempotency": str(uuid4()), # UUID único por factura
}
payload = {
"tipoDeComprobante": "I", # I = Ingreso
"receptor": {
"rfc": "XAXX010101000",
"nombre": "PUBLICO EN GENERAL",
"regimenFiscal": "616", # Sin obligaciones fiscales
"domicilioFiscal": "06500",
"email": "cliente@ejemplo.com",
},
"usoCFDI": "G03", # Gastos en general
"metodoPago": "PUE", # Pago en Una Exhibición
"formaPago": "03", # Transferencia electrónica
"fecha": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
"moneda": "MXN",
"exportacion": "01", # No aplica
"lugarExpedicion": "06500",
"conceptos": [
{
"claveProdServ": "81111500", # Servicios de ingeniería de software
"claveUnidad": "E48", # Unidad de servicio
"descripcion": "Desarrollo de Software - Sistema Web a Medida",
"cantidad": 1,
"valorUnitario": 15000.00,
"objetoImp": "02", # Sí objeto de impuesto
"impuestos": {
"traslados": [
{
"base": 15000.00,
"impuesto": "002", # IVA
"tipoFactor": "Tasa",
"tasaOCuota": 0.16, # 16%
}
]
},
}
],
}
try {
response = requests.post(api_url, json=payload, headers=headers, timeout=30)
response.raise_for_status() # Lanza excepción si status >= 400
data = response.json()
print("Factura timbrada exitosamente")
print(f"UUID Fiscal: {data['cfdiUuid']}")
print(f"Descargar XML: {data['xmlS3Url']}")
print(f"Descargar PDF: {data['pdfS3Url']}")
return data
except requests.exceptions.HTTPError as e:
print(f"Error HTTP: {e.response.status_code}")
print(f"Respuesta: {e.response.json()}")
raise
except requests.exceptions.RequestException as e:
print(f"Error de conexion: {str(e)}")
raise
if __name__ == "__main__":
crear_factura()
pip install requests
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Ramsey\Uuid\Uuid;
/**
* Crea y timbra una factura de ingreso usando la API de Lummy
*
* @return array Respuesta con invoiceId, cfdiUuid, URLs de descarga
* @throws Exception Si falla la autenticación o timbrado
*/
function crearFactura(): array
{
$apiUrl = 'https://sandbox.lummy.io/invoices';
$orgId = getenv('LUMMY_ORG_ID');
$apiKey = getenv('LUMMY_API_KEY');
if (!$orgId || !$apiKey) {
throw new Exception('Debes definir LUMMY_ORG_ID y LUMMY_API_KEY');
}
$client = new Client([
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
'x-organization-id' => $orgId,
'x-api-key' => $apiKey,
'x-idempotency' => Uuid::uuid4()->toString(), // UUID único por factura
],
]);
$payload = [
'tipoDeComprobante' => 'I', // I = Ingreso
'receptor' => [
'rfc' => 'XAXX010101000',
'nombre' => 'PUBLICO EN GENERAL',
'regimenFiscal' => '616', // Sin obligaciones fiscales
'domicilioFiscal' => '06500',
'email' => 'cliente@ejemplo.com',
],
'usoCFDI' => 'G03', // Gastos en general
'metodoPago' => 'PUE', // Pago en Una Exhibición
'formaPago' => '03', // Transferencia electrónica
'fecha' => date('Y-m-d\TH:i:s'), // Formato: 2025-11-19T10:00:00
'moneda' => 'MXN',
'exportacion' => '01', // No aplica
'lugarExpedicion' => '06500',
'conceptos' => [
[
'claveProdServ' => '81111500', // Servicios de ingeniería de software
'claveUnidad' => 'E48', // Unidad de servicio
'descripcion' => 'Desarrollo de Software - Sistema Web a Medida',
'cantidad' => 1,
'valorUnitario' => 15000.00,
'objetoImp' => '02', // Sí objeto de impuesto
'impuestos' => [
'traslados' => [
[
'base' => 15000.00,
'impuesto' => '002', // IVA
'tipoFactor' => 'Tasa',
'tasaOCuota' => 0.16, // 16%
],
],
],
],
],
];
try {
$response = $client->post($apiUrl, [
'json' => $payload,
]);
$data = json_decode($response->getBody()->getContents(), true);
echo "Factura timbrada exitosamente\n";
echo "UUID Fiscal: {$data['cfdiUuid']}\n";
echo "Descargar XML: {$data['xmlS3Url']}\n";
echo "Descargar PDF: {$data['pdfS3Url']}\n";
return $data;
} catch (RequestException $e) {
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A';
$errorBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'Sin detalles';
echo "Error HTTP {$statusCode}\n";
echo "Respuesta: {$errorBody}\n";
throw $e;
}
}
// Ejecutar
try {
crearFactura();
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
composer require guzzlehttp/guzzle ramsey/uuid
Response
Respuesta Exitosa (HTTP 201 Created)
Todas las respuestas siguen el formato estándar StandardResponse:
{
"requestId": "abc123-def456",
"data": {
"invoiceId": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"cfdiUuid": "cfa52b8b-93f2-4e6b-8c73-64ad88deb17c",
"verificationUrl": "https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?id=cfa52b8b-93f2-4e6b-8c73-64ad88deb17c",
"xmlUrl": "https://lummy-invoices.s3.amazonaws.com/cfdi/cfa52b8b-93f2-4e6b-8c73-64ad88deb17c.xml",
"status": "stamped"
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
El cfdiUuid es el Folio Fiscal oficial. Este UUID debe aparecer en todas las representaciones impresas del CFDI y es el identificador único que tu cliente usará para verificar la factura en el SAT.
El archivo PDF se genera de forma asíncrona después del timbrado y se envía automáticamente por correo electrónico al receptor. Puedes obtener la URL del PDF consultando el detalle de la factura con GET /invoices/{invoiceId}.
Respuesta Pendiente de Timbrado (HTTP 201 Created)
Si el PAC no responde o hay un error temporal, la factura se guarda y se reintenta automáticamente:
{
"requestId": "abc123-def456",
"data": {
"invoiceId": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"cfdiUuid": "",
"verificationUrl": "",
"xmlUrl": "",
"status": "pending_stamping",
"retryInfo": {
"nextRetryAt": "2025-01-20T10:05:00.000Z",
"attempt": 1,
"maxAttempts": 5
}
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
El sistema reintenta automáticamente el timbrado con backoff exponencial. No necesitas hacer nada adicional. Puedes consultar el estado de la factura con GET /invoices/{invoiceId}.
Respuestas de Error
Todas las respuestas de error siguen el formato estándar StandardResponse:
{
"requestId": "abc123-def456",
"error": {
"message": "Descripción del error",
"code": "ERROR_CODE",
"status": 400
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
400 Bad Request - Datos Inválidos
{
"requestId": "abc123-def456",
"error": {
"message": "RFC inválido: debe tener 12 o 13 caracteres",
"code": "ValidationError",
"status": 400
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Causas comunes:
- RFC con formato incorrecto
- Claves de catálogo SAT inválidas (ej.
claveProdServ,usoCFDI) - Fechas fuera del rango permitido por el SAT
- Campos requeridos faltantes
- Header
x-idempotencyfaltante
401 Unauthorized - Autenticación Inválida
{
"requestId": "abc123-def456",
"error": {
"message": "Token de autenticación inválido o expirado",
"code": "UnauthorizedException",
"status": 401
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Solución: Verifica que tu API Key o JWT sea válido y no haya expirado.
403 Forbidden - Sin Permisos
{
"requestId": "abc123-def456",
"error": {
"message": "User is not allowed to stamp invoices",
"code": "ForbiddenException",
"status": 403
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Causas comunes:
- El usuario no tiene el scope
invoices:create - La suscripción está inactiva o expirada
- El saldo de timbres es insuficiente
- El actor no pertenece a la organización especificada
404 Not Found - Recurso No Encontrado
{
"requestId": "abc123-def456",
"error": {
"message": "No se encontró CSD activo para la sucursal",
"code": "BranchNotFoundError",
"status": 404
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Causas comunes:
- La sucursal no existe o no pertenece a la organización
- No hay un CSD vigente cargado para la sucursal
- El cliente especificado (
clienteId) no existe
409 Conflict - Conflicto de Idempotencia
{
"requestId": "abc123-def456",
"error": {
"message": "Invoice with idempotencyKey abc-123 failed previously. Please use a different idempotencyKey to retry.",
"code": "ConflictException",
"status": 409
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Causas comunes:
- La clave de idempotencia ya fue usada para una factura que falló
- El folio especificado ya existe para la organización (conflicto de concurrencia)
Solución: Genera un nuevo UUID para x-idempotency si quieres reintentar.
422 Unprocessable Entity - Rechazo del PAC/SAT
{
"requestId": "abc123-def456",
"error": {
"message": "CFDI40139 - El RFC del receptor no existe en la lista de RFC inscritos no cancelados del SAT",
"code": "PacRejectionError",
"status": 422
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Causas comunes:
- RFC del receptor no registrado en el SAT
- Certificado de sello digital (CSD) vencido o revocado
- Errores de validación de reglas del SAT (CFDI40xxx)
- RFC del emisor en lista 69-B del SAT
429 Too Many Requests - Rate Limit Excedido
{
"requestId": "abc123-def456",
"error": {
"message": "ThrottlerException: Too Many Requests",
"code": "ThrottlerException",
"status": 429
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Solución: Espera un minuto antes de reintentar. El límite es de 30 solicitudes por minuto.
500 Internal Server Error - Error Interno
{
"requestId": "abc123-def456",
"error": {
"message": "An unexpected internal server error occurred.",
"code": "INTERNAL_SERVER_ERROR",
"status": 500
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices",
"method": "POST"
}
Nota: Si el timbrado falla después de guardar la factura, el sistema la marcará como pending_stamping y reintentará automáticamente. El saldo de timbres no se consume hasta que el timbrado sea exitoso.
Campos Obligatorios Explicados
exportacion
- Valores:
01(No aplica),02(Definitiva),03(Temporal) - Para ventas nacionales, siempre usa
'01'
objetoImp
01: No objeto de impuesto02: Sí objeto de impuesto (requiere nodoimpuestos)03: Sí objeto de impuesto y NO obligado al desglose
claveProdServ
Clave del catálogo SAT de productos y servicios:
81111500: Servicios de ingeniería de software84111506: Servicios de consultoría en tecnología- Buscar más claves aquí
claveUnidad
Unidad de medida del SAT:
E48: Unidad de servicioH87: PiezaKGM: Kilogramo
Notas Importantes
Cálculo Automático de Totales
No necesitas enviar los campos Subtotal, Total, ni Importe de conceptos. Nuestro sistema los calcula automáticamente según las reglas del SAT para garantizar precisión.
Zona Horaria
El campo fecha debe enviarse en hora local del lugar de expedición. El sistema ajustará automáticamente la zona horaria según el código postal de lugarExpedicion.
Transaccionalidad
Si el timbrado falla después de validar los datos, NO se consume un timbre de tu saldo. El sistema usa transacciones atómicas para garantizar consistencia.
Siguientes Pasos
Soporte
¿Necesitas ayuda? Contáctanos:
- Email: soporte@lummy.io
- Documentación: docs.lummy.io
Objeto: ReceptorDto
Objeto: ConceptoDto
Objeto: ConceptoImpuestosDto
Objeto: ConceptoTrasladoDto
Objeto: ConceptoRetencionDto
Objeto: CfdiRelacionadosDto
Nodo para especificar CFDI relacionados (ej. notas de crédito, sustituciones).
Ejemplo:
{
"tipoRelacion": "04",
"uuids": ["fdb057e9-890b-487e-953b-05b1c59b4e3c"]
}
Tipos de Relación Comunes:
01: Nota de crédito de los documentos relacionados02: Nota de débito de los documentos relacionados03: Devolución de mercancía04: Sustitución de los CFDI previos
Objeto: InformacionGlobalDto
Nodo requerido para facturas globales (RFC XAXX010101000 - público en general).
Ejemplo:
{
"periodicidad": "01",
"meses": "01",
"anio": 2025
}
Periodicidad:
01: Diaria02: Semanal03: Quincenal04: Mensual05: Bimestral
Objeto: ACuentaTercerosDto
Nodo para operaciones a cuenta de terceros.
Objeto: InformacionAduaneraDto
Nodo para información de pedimentos de importación.
Formato del pedimento: AA BB CCCC DDDDDDD (ejemplo: 21 47 3840 8000936)
Objeto: ParteDto
Nodo para especificar partes o componentes de un concepto.
Objeto: EspacioDeNombreDto
Nodo para definir namespaces XML requeridos por addendas.
Ejemplo:
{
"prefijo": "miAddenda",
"uri": "http://www.miempresa.com/addenda/v1",
"ubicacionEsquema": "http://www.miempresa.com/addenda/v1/addenda.xsd"
}
Objeto: ComplementoGenericoDto
Nodo para complementos fiscales adicionales (Comercio Exterior, etc.).
Ejemplo:
{
"xml": "<cce20:ComercioExterior Version=\"2.0\" ... />"
}