Sellar Factura (Timbrar XML Pre-sellado)
Recibe un XML CFDI ya sellado por tu infraestructura y lo envia al PAC para timbrado. Este es el segundo paso del flujo de dos pasos.
Privacidad de Datos
Este endpoint NO almacena datos:
- No se guarda el XML en la base de datos
- No se registran datos fiscales en logs
- El XML solo existe en memoria durante el procesamiento
- Solo se retorna el CFDI timbrado al cliente
Endpoint
POST https://api.lummy.io/v1/invoices/stamp
| Entorno | URL |
|---|---|
| Producción | https://api.lummy.io/v1/invoices/stamp |
| Sandbox | https://sandbox.lummy.io/v1/invoices/stamp |
Prerequisitos
Antes de usar este endpoint, debes:
- Llamar a
POST /invoices/preparepara obtener el XML sin sellar y la cadena original - Firmar la cadena original con tu FIEL/CSD usando SHA256 con RSA
- Insertar el sello (firma) en el atributo
Sellodel XML
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": string,requerido
↳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.
"x-idempotency": string (UUID)requerido
↳UUID único para garantizar idempotencia en la solicitud de timbrado. Si se reenvía la misma petición con el mismo UUID, se retornará el resultado del timbrado original sin crear un nuevo CFDI. Previene timbrados duplicados en caso de reintentos por errores de red.
}
Body Parameters
{
"sealedXml": stringrequerido
↳XML completo del CFDI con el atributo "Sello" (firma digital) ya incluido. El sello debe haber sido generado firmando la cadena original con tu certificado de sello digital (CSD) usando el algoritmo SHA256 con RSA. El PAC validará que el sello corresponda al certificado antes de timbrar.
}
Como Insertar el Sello en el XML
El sello debe insertarse como el valor del atributo Sello en el nodo raiz <cfdi:Comprobante>:
<?xml version="1.0" encoding="UTF-8"?>
<cfdi:Comprobante
xmlns:cfdi="http://www.sat.gob.mx/cfd/4"
Version="4.0"
Sello="TU_SELLO_BASE64_AQUI"
NoCertificado="00001000000504465028"
Certificado="MIIF..."
... >
Ejemplos de Codigo
- cURL
- Node.js (TypeScript)
- Python
- PHP (Guzzle)
# El XML sellado debe estar en una variable o archivo
SEALED_XML='<?xml version="1.0" encoding="UTF-8"?><cfdi:Comprobante ... Sello="abc123..." ... />'
curl -X POST https://sandbox.lummy.io/invoices/stamp \
-H "Content-Type: application/json" \
-H "x-organization-id: ${LUMMY_ORG_ID}" \
-H "x-api-key: ${LUMMY_API_KEY}" \
-H "x-idempotency: $(uuidgen)" \
-d "{\"sealedXml\": $(echo $SEALED_XML | jq -Rs .)}"
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
interface StampResponse {
cfdi: string;
uuid: string;
status: 'STAMPED';
xmlS3Url?: string;
pdfS3Url?: string;
}
async function sellarFactura(sealedXml: string): Promise<StampResponse> {
const API_URL = 'https://sandbox.lummy.io/invoices/stamp';
const response = await axios.post<StampResponse>(
API_URL,
{ sealedXml },
{
headers: {
'Content-Type': 'application/json',
'x-organization-id': process.env.LUMMY_ORG_ID!,
'x-api-key': process.env.LUMMY_API_KEY!,
'x-idempotency': uuidv4(),
},
}
);
console.log('CFDI timbrado exitosamente');
console.log('UUID:', response.data.uuid);
return response.data;
}
// Flujo completo
async function emitirFacturaConHSM() {
// Paso 1: Preparar factura
const { unsignedXml, originalString } = await prepararFactura();
// Paso 2: Firmar con tu HSM/FIEL (implementacion propia)
const sello = await firmarConHSM(originalString);
// Paso 3: Insertar sello en XML
const sealedXml = unsignedXml.replace(
'Sello=""',
`Sello="${sello}"`
);
// Paso 4: Enviar a timbrar
const resultado = await sellarFactura(sealedXml);
return resultado;
}
import os
import requests
from uuid import uuid4
def sellar_factura(sealed_xml: str) -> dict:
"""
Envia el XML sellado al PAC para timbrado.
"""
api_url = "https://sandbox.lummy.io/invoices/stamp"
headers = {
"Content-Type": "application/json",
"x-organization-id": os.getenv("LUMMY_ORG_ID"),
"x-api-key": os.getenv("LUMMY_API_KEY"),
"x-idempotency": str(uuid4()),
}
payload = {"sealedXml": sealed_xml}
response = requests.post(api_url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
print("CFDI timbrado exitosamente")
print(f"UUID: {data['uuid']}")
return data
def emitir_factura_con_hsm():
"""
Flujo completo: preparar -> sellar con HSM -> timbrar
"""
# Paso 1: Preparar factura
prepare_result = preparar_factura()
unsigned_xml = prepare_result["unsignedXml"]
original_string = prepare_result["originalString"]
# Paso 2: Firmar con tu HSM/FIEL
sello = firmar_con_hsm(original_string) # Tu implementacion
# Paso 3: Insertar sello en XML
sealed_xml = unsigned_xml.replace('Sello=""', f'Sello="{sello}"')
# Paso 4: Timbrar
resultado = sellar_factura(sealed_xml)
return resultado
if __name__ == "__main__":
# Ejemplo con XML ya sellado
with open("factura_sellada.xml", "r") as f:
sealed_xml = f.read()
sellar_factura(sealed_xml)
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use Ramsey\Uuid\Uuid;
function sellarFactura(string $sealedXml): 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'),
'x-idempotency' => Uuid::uuid4()->toString(),
],
]);
$response = $client->post('/invoices/stamp', [
'json' => ['sealedXml' => $sealedXml],
]);
$data = json_decode($response->getBody(), true);
echo "CFDI timbrado exitosamente\n";
echo "UUID: " . $data['uuid'] . "\n";
return $data;
}
/**
* Flujo completo: preparar -> sellar con HSM -> timbrar
*/
function emitirFacturaConHSM(): array
{
// Paso 1: Preparar factura
$prepareResult = prepararFactura();
$unsignedXml = $prepareResult['unsignedXml'];
$originalString = $prepareResult['originalString'];
// Paso 2: Firmar con tu HSM/FIEL
$sello = firmarConHSM($originalString); // Tu implementacion
// Paso 3: Insertar sello en XML
$sealedXml = str_replace('Sello=""', 'Sello="' . $sello . '"', $unsignedXml);
// Paso 4: Timbrar
return sellarFactura($sealedXml);
}
// Ejemplo con XML ya sellado
$sealedXml = file_get_contents('factura_sellada.xml');
sellarFactura($sealedXml);
Respuesta Exitosa (HTTP 201)
Todas las respuestas siguen el formato estándar StandardResponse.
{
"requestId": "abc123-def456",
"data": {
"cfdi": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><cfdi:Comprobante ... ><cfdi:Complemento><tfd:TimbreFiscalDigital ... UUID=\"cfa52b8b-93f2-4e6b-8c73-64ad88deb17c\" ... /></cfdi:Complemento></cfdi:Comprobante>",
"uuid": "cfa52b8b-93f2-4e6b-8c73-64ad88deb17c",
"status": "STAMPED"
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices/stamp",
"method": "POST"
}
Campos de la Respuesta
{
"cfdi": string,requerido
↳XML completo del CFDI timbrado que incluye el complemento TimbreFiscalDigital del SAT. Este complemento contiene el UUID fiscal, la fecha y hora de certificación, el sello del SAT, el RFC del proveedor de certificación (PAC) y el número de certificado del SAT. Es el documento fiscal oficial que debe conservarse durante 5 años.
"uuid": string (UUID),requerido
↳Folio fiscal (UUID) asignado por el SAT al momento del timbrado. Es el identificador único e irrepetible del CFDI ante el SAT y se utiliza para todas las operaciones de validación, verificación y cancelación del comprobante fiscal. Se genera por el PAC y es registrado en la base de datos del SAT.
"status": stringrequerido
↳Estado del proceso de timbrado. Siempre será "STAMPED" en caso de éxito, indicando que el CFDI fue timbrado exitosamente y tiene validez fiscal ante el SAT.
}
Respuestas de Error
| Codigo | Descripcion |
|---|---|
| 400 | XML invalido, sello faltante, o firma incorrecta. |
| 401 | API Key invalida o no autorizada. |
| 404 | Organizacion no encontrada. |
| 409 | El header x-idempotency ya fue usado. |
| 422 | El PAC rechazo el CFDI (errores de validacion SAT). |
Error 400: Sello Invalido
{
"requestId": "abc123-def456",
"error": {
"message": "El sello digital no corresponde al certificado",
"code": "ValidationError",
"status": 400
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices/stamp",
"method": "POST"
}
Causas comunes:
- La cadena original firmada no coincide con la del XML
- Se uso un algoritmo de firma incorrecto (debe ser SHA256 con RSA)
- El certificado en el XML no corresponde a la llave privada usada
Error 422: Rechazo del PAC
{
"requestId": "abc123-def456",
"error": {
"message": "CFDI40139 - El RFC del receptor no existe en la lista de RFC inscritos no cancelados del SAT",
"code": "UnprocessableEntityException",
"status": 422
},
"timestamp": "2025-01-20T10:00:00.000Z",
"path": "/invoices/stamp",
"method": "POST"
}
Comparativa: Flujo Completo
| Paso | Tu Sistema | Lummy |
|---|---|---|
| 1. Enviar datos | POST /invoices/prepare | Genera XML y cadena original |
| 2. Firmar | Tu HSM/FIEL | - |
| 3. Insertar sello | Tu codigo | - |
| 4. Timbrar | POST /invoices/stamp | Envia al PAC, retorna UUID |
Notas Importantes
Almacenamiento
Responsabilidad del almacenamiento
Como este flujo NO almacena el CFDI en Lummy, tu eres responsable de:
- Guardar el XML timbrado
- Generar y almacenar el PDF
- Conservar los archivos por 5 anos (requisito SAT)
Idempotencia
El header x-idempotency evita timbrados duplicados:
- Si envias el mismo UUID de idempotencia, recibiras el resultado anterior
- Genera un nuevo UUID para cada factura diferente
Validaciones del PAC
El PAC valida antes de timbrar:
- Estructura del XML (XSD del SAT)
- Vigencia del certificado
- Que el sello corresponda al certificado
- RFCs en lista negra (69-B)
- Reglas de negocio del SAT