Preparar Factura
Genera el XML sin sellar y la cadena original para que selles el CFDI con tu propia infraestructura (HSM, FIEL corporativa).
Flujo de Dos Pasos vs Todo-en-Uno
Lummy ofrece dos formas de emitir facturas:
| Metodo | Endpoint | Quien sella | Uso recomendado |
|---|---|---|---|
| Todo-en-uno | POST /invoices | Lummy | Mayoria de casos |
| Dos pasos | POST /invoices/prepare + POST /invoices/stamp | Tu infraestructura | Empresas con HSM propio |
Diagrama de Secuencia: Flujo de Dos Pasos
Privacidad de Datos
Los endpoints /invoices/prepare y /invoices/stamp NO almacenan datos:
- No se guarda informacion en la base de datos
- No se registran datos fiscales en logs
- Los datos solo existen en memoria durante el procesamiento
- Ideal para empresas con requisitos estrictos de privacidad
Endpoint
POST https://api.lummy.io/v1/invoices/prepare
| Entorno | URL |
|---|---|
| Producción | https://api.lummy.io/v1/invoices/prepare |
| Sandbox | https://sandbox.lummy.io/v1/invoices/prepare |
Headers
{
"x-organization-id": string (UUID),requerido
↳Identificador único de tu organización en Lummy. Este valor se obtiene al crear tu cuenta y es necesario para todas las operaciones relacionadas con facturación electrónica.
"x-api-key": stringrequerido
↳Clave de API para autenticación. Se genera desde el panel de Lummy y debe mantenerse confidencial. Es una alternativa al token Bearer JWT para autenticar tus solicitudes.
}
Estructura del Request
El cuerpo es identico al endpoint POST /invoices. Consulta la documentacion de Emitir Factura para ver todos los campos disponibles.
Campos Principales
{
"tipoDeComprobante": string,requerido
↳Clave del tipo de comprobante según el catálogo c_TipoDeComprobante del SAT. Valores: "I" (Ingreso para ventas), "E" (Egreso para devoluciones y descuentos), "T" (Traslado de mercancías), "P" (Pago en parcialidades o diferido). Define la naturaleza fiscal de la operación.
"receptor": object,opcional
↳Objeto que contiene los datos fiscales del receptor del CFDI. Incluye RFC, nombre, régimen fiscal y domicilio fiscal. Es obligatorio si no se proporciona clienteId. Los datos deben estar registrados en el padrón del SAT.
"fecha": string (ISO 8601),requerido
↳Fecha y hora de expedición del comprobante en formato ISO 8601 (YYYY-MM-DDTHH:mm:ss). Debe estar dentro de las 72 horas anteriores a la fecha de timbrado y no puede ser posterior a la fecha actual. El SAT valida que esta fecha sea coherente con la vigencia del CSD.
"moneda": string,requerido
↳Clave de la moneda del comprobante según el catálogo c_Moneda del SAT. Ejemplos: "MXN" (peso mexicano), "USD" (dólar estadounidense), "EUR" (euro), "XXX" (sin moneda para CFDIs de traslado). Si es diferente de MXN, se requiere el tipo de cambio.
"exportacion": string,requerido
↳Clave que indica si el comprobante ampara una operación de exportación según catálogo c_Exportacion. Valores: "01" (no aplica para operación nacional), "02" (exportación definitiva), "03" (exportación temporal), "04" (exportación de servicios). Normalmente se usa "01" para ventas nacionales.
"lugarExpedicion": string,requerido
↳Código postal del lugar de expedición del comprobante. Debe corresponder a un código postal válido en el catálogo c_CodigoPostal del SAT y generalmente es el código postal del domicilio fiscal de la matriz o sucursal emisora. Se utiliza para efectos fiscales y estadísticos.
"conceptos": arrayrequerido
↳Arreglo de conceptos que describe los bienes o servicios facturados. Cada concepto debe incluir: claveProdServ (catálogo SAT), claveUnidad, descripción, cantidad, valorUnitario, objetoImp (si aplican impuestos) e impuestos (traslados y retenciones). Es el detalle principal del comprobante fiscal.
}
Ejemplos de Codigo
- cURL
- Node.js (TypeScript)
- Python
- PHP (Guzzle)
curl -X POST https://sandbox.lummy.io/invoices/prepare \
-H "Content-Type: application/json" \
-H "x-organization-id: ${LUMMY_ORG_ID}" \
-H "x-api-key: ${LUMMY_API_KEY}" \
-d '{
"tipoDeComprobante": "I",
"receptor": {
"rfc": "XAXX010101000",
"nombre": "PUBLICO EN GENERAL",
"regimenFiscal": "616",
"domicilioFiscal": "06500"
},
"usoCFDI": "G03",
"metodoPago": "PUE",
"formaPago": "03",
"fecha": "2025-01-20T10:00:00",
"moneda": "MXN",
"exportacion": "01",
"lugarExpedicion": "06500",
"conceptos": [
{
"claveProdServ": "81111500",
"claveUnidad": "E48",
"descripcion": "Servicio de consultoria",
"cantidad": 1,
"valorUnitario": 10000.00,
"objetoImp": "02",
"impuestos": {
"traslados": [{
"base": 10000.00,
"impuesto": "002",
"tipoFactor": "Tasa",
"tasaOCuota": 0.16
}]
}
}
]
}'
import axios from 'axios';
import * as crypto from 'crypto';
import * as fs from 'fs';
interface PrepareResponse {
unsignedXml: string;
originalString: string;
}
async function prepararFactura(): Promise<PrepareResponse> {
const API_URL = 'https://sandbox.lummy.io/invoices/prepare';
const payload = {
tipoDeComprobante: 'I',
receptor: {
rfc: 'XAXX010101000',
nombre: 'PUBLICO EN GENERAL',
regimenFiscal: '616',
domicilioFiscal: '06500',
},
usoCFDI: 'G03',
metodoPago: 'PUE',
formaPago: '03',
fecha: new Date().toISOString().slice(0, 19),
moneda: 'MXN',
exportacion: '01',
lugarExpedicion: '06500',
conceptos: [{
claveProdServ: '81111500',
claveUnidad: 'E48',
descripcion: 'Servicio de consultoria',
cantidad: 1,
valorUnitario: 10000.00,
objetoImp: '02',
impuestos: {
traslados: [{
base: 10000.00,
impuesto: '002',
tipoFactor: 'Tasa',
tasaOCuota: 0.16,
}],
},
}],
};
const response = await axios.post<PrepareResponse>(API_URL, payload, {
headers: {
'Content-Type': 'application/json',
'x-organization-id': process.env.LUMMY_ORG_ID!,
'x-api-key': process.env.LUMMY_API_KEY!,
},
});
console.log('XML sin sellar obtenido');
console.log('Cadena original:', response.data.originalString.substring(0, 100) + '...');
return response.data;
}
// Paso 2: Sellar con tu FIEL/HSM
function sellarConFiel(originalString: string, privateKeyPath: string, password: string): string {
const privateKey = fs.readFileSync(privateKeyPath);
const sign = crypto.createSign('RSA-SHA256');
sign.update(originalString);
sign.end();
const signature = sign.sign({
key: privateKey,
passphrase: password,
});
return signature.toString('base64');
}
prepararFactura();
import os
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
import base64
def preparar_factura():
"""
Paso 1: Obtener XML sin sellar y cadena original de Lummy.
"""
api_url = "https://sandbox.lummy.io/invoices/prepare"
headers = {
"Content-Type": "application/json",
"x-organization-id": os.getenv("LUMMY_ORG_ID"),
"x-api-key": os.getenv("LUMMY_API_KEY"),
}
payload = {
"tipoDeComprobante": "I",
"receptor": {
"rfc": "XAXX010101000",
"nombre": "PUBLICO EN GENERAL",
"regimenFiscal": "616",
"domicilioFiscal": "06500",
},
"usoCFDI": "G03",
"metodoPago": "PUE",
"formaPago": "03",
"fecha": "2025-01-20T10:00:00",
"moneda": "MXN",
"exportacion": "01",
"lugarExpedicion": "06500",
"conceptos": [{
"claveProdServ": "81111500",
"claveUnidad": "E48",
"descripcion": "Servicio de consultoria",
"cantidad": 1,
"valorUnitario": 10000.00,
"objetoImp": "02",
"impuestos": {
"traslados": [{
"base": 10000.00,
"impuesto": "002",
"tipoFactor": "Tasa",
"tasaOCuota": 0.16,
}],
},
}],
}
response = requests.post(api_url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
print("XML sin sellar obtenido")
print(f"Cadena original: {data['originalString'][:100]}...")
return data
def sellar_con_fiel(original_string: str, key_path: str, password: str) -> str:
"""
Paso 2: Sellar la cadena original con tu FIEL/CSD.
"""
with open(key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=password.encode(),
backend=default_backend()
)
signature = private_key.sign(
original_string.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256()
)
return base64.b64encode(signature).decode("utf-8")
if __name__ == "__main__":
result = preparar_factura()
# sello = sellar_con_fiel(result["originalString"], "mi_fiel.key", "password")
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
function prepararFactura(): array
{
$client = new Client([
'base_uri' => 'https://sandbox.lummy.io',
'headers' => [
'Content-Type' => 'application/json',
'x-organization-id' => getenv('LUMMY_ORG_ID'),
'x-api-key' => getenv('LUMMY_API_KEY'),
],
]);
$payload = [
'tipoDeComprobante' => 'I',
'receptor' => [
'rfc' => 'XAXX010101000',
'nombre' => 'PUBLICO EN GENERAL',
'regimenFiscal' => '616',
'domicilioFiscal' => '06500',
],
'usoCFDI' => 'G03',
'metodoPago' => 'PUE',
'formaPago' => '03',
'fecha' => date('Y-m-d\TH:i:s'),
'moneda' => 'MXN',
'exportacion' => '01',
'lugarExpedicion' => '06500',
'conceptos' => [[
'claveProdServ' => '81111500',
'claveUnidad' => 'E48',
'descripcion' => 'Servicio de consultoria',
'cantidad' => 1,
'valorUnitario' => 10000.00,
'objetoImp' => '02',
'impuestos' => [
'traslados' => [[
'base' => 10000.00,
'impuesto' => '002',
'tipoFactor' => 'Tasa',
'tasaOCuota' => 0.16,
]],
],
]],
];
$response = $client->post('/invoices/prepare', ['json' => $payload]);
$data = json_decode($response->getBody(), true);
echo "XML sin sellar obtenido\n";
echo "Cadena original: " . substr($data['originalString'], 0, 100) . "...\n";
return $data;
}
/**
* Paso 2: Sellar con tu FIEL/CSD
*/
function sellarConFiel(string $originalString, string $keyPath, string $password): string
{
$privateKey = openssl_pkey_get_private(
file_get_contents($keyPath),
$password
);
openssl_sign($originalString, $signature, $privateKey, OPENSSL_ALGO_SHA256);
return base64_encode($signature);
}
$result = prepararFactura();
Respuesta Exitosa (HTTP 200)
Todas las respuestas siguen el formato estándar StandardResponse.
{
"requestId": "abc123-def456",
"data": {
"unsignedXml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><cfdi:Comprobante xmlns:cfdi=\"http://www.sat.gob.mx/cfd/4\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" Version=\"4.0\" ... NoCertificado=\"00001000000504465028\" Certificado=\"MIIF...\" ... />",
"originalString": "||4.0|2025-01-20T10:00:00|01|00001000000504465028|10000.00|MXN|11600.00|I|PUE|06500|XAXX010101000|PUBLICO EN GENERAL|616|06500|G03|81111500|1|E48|Servicio de consultoria|10000.00|10000.00|02|002|Tasa|0.16|1600.00|002|Tasa|1600.00||"
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices/prepare",
"method": "POST"
}
Campos de la Respuesta
{
"unsignedXml": string,requerido
↳XML del CFDI completo sin el atributo "Sello" (firma digital). Contiene todos los datos del comprobante estructurados según el estándar del SAT: emisor, receptor, conceptos, impuestos y certificado (CSD). Este XML está listo para ser firmado digitalmente con tu FIEL o HSM corporativo.
"originalString": stringrequerido
↳Cadena original del comprobante según el formato establecido por el SAT. Es una concatenación de campos específicos del CFDI separados por pipes (||). Esta cadena debe ser firmada digitalmente usando el algoritmo SHA256 con RSA y tu certificado de sello digital (CSD) para generar el sello que se insertará en el XML.
}
Respuestas de Error
| Codigo | Descripcion |
|---|---|
| 400 | Datos de entrada invalidos (RFC, fechas, catalogos SAT). |
| 401 | API Key invalida o no autorizada. |
| 404 | Organizacion o sucursal no encontrada. |
{
"requestId": "abc123-def456",
"error": {
"message": "RFC invalido: debe tener 12 o 13 caracteres",
"code": "ValidationError",
"status": 400
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices/prepare",
"method": "POST"
}
Siguiente Paso
Una vez que tengas el XML sellado, envialo al endpoint Sellar Factura para obtener el CFDI timbrado.