Sin resultados para tu búsqueda. Limpiar.
API de Timbrado CFDI 4.0
Timbra comprobantes fiscales 4.0 directamente desde tu sistema usando la infraestructura de Facter. Tú nos envías el JSON del CFDI; nosotros sellamos, timbramos con el PAC y te devolvemos el XML timbrado.
El modelo de actores
El API se organiza alrededor de tres piezas:
- Cliente principal — quien activa el API en su cuenta Facter y compra los timbres prepago. Es dueño de las API keys.
- Emisores — cada empresa que emite CFDIs (tu cliente / tenant en tu sistema). Cada emisor tiene su propio RFC y CSD.
- Pool de timbres — el saldo del principal. Todos los emisores consumen del mismo pool: el principal paga, los emisores timbran.
Tu cuenta principal también puede timbrar con su propio RFC: queda registrada automáticamente como emisor al activar tu API. A diferencia de los emisores hijos, el principal consume su propio saldo PREPAGO (la misma bolsa que financia el pool, no se descuenta distinto) y no puede desvincularse de sí mismo. En GET /emisores lo identificas por is_principal: true.
URL base
Hay dos ambientes, cada uno con su propia URL (ver Ambientes):
- Demo (pruebas):
https://demo.facter.com.mx/api/ext/v1 - Producción:
https://v2.facter.com.mx/api/ext/v1
Todas las respuestas siguen la convención {status, message, data?, errors?} e incluyen el header X-Request-Id para soporte.
Primeros pasos (Quickstart)
Objetivo: tu primer timbrado en el ambiente demo en menos de 30 minutos, sin acompañamiento. Prueba siempre en demo antes de pasar a producción.
- Crea tu cuenta demo y genera tu key. Regístrate en
https://demo.facter.com.mx, ve a Mi cuenta → API, presiona Activar (autoservicio) y genera una keyfct_live_*. En demo el timbrado usa el sandbox de pruebas sin cobro real. - Da de alta un emisor. Usa el RFC de pruebas del SAT
EKU9003173C9(ver cómo obtener acceso).curl -X POST https://demo.facter.com.mx/api/ext/v1/emisores \ -H "Authorization: Bearer fct_live_xxxx" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"external_ref":"tenant-externo-42","razon_social":"ESCUELA KEMPER URGATE","rfc":"EKU9003173C9","regimen_fiscal":"601","codigo_postal":"64000"}' - Sube el CSD de pruebas del emisor (archivos
.cery.keyen base64 + contraseña) con PUT/emisores/EKU9003173C9/csd. - Timbra tu primer CFDI en demo. La respuesta trae
"environment":"demo"y el CFDI queda persistido en la BD demo.curl -X POST https://demo.facter.com.mx/api/ext/v1/cfdis \ -H "Authorization: Bearer fct_live_xxxxxxxxxxxxxxxx" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"emisor_rfc":"EKU9003173C9","external_ref":"REF-FAC-000123","fecha_emision":null,"cfdi":{"Version":"4.0","Serie":"A","Folio":"123","FormaPago":"01","MetodoPago":"PUE","CondicionesDePago":null,"SubTotal":"1000.00","Descuento":null,"Moneda":"MXN","Total":"1160.00","TipoDeComprobante":"I","Exportacion":"01","LugarExpedicion":"64000","Emisor":{"Rfc":"EKU9003173C9","Nombre":"ESCUELA KEMPER URGATE","RegimenFiscal":"601"},"Receptor":{"Rfc":"XAXX010101000","Nombre":"PUBLICO EN GENERAL","DomicilioFiscalReceptor":"64000","RegimenFiscalReceptor":"616","UsoCFDI":"S01"},"Conceptos":[{"ClaveProdServ":"01010101","NoIdentificacion":"SKU-01","Cantidad":"1.000000","ClaveUnidad":"H87","Unidad":"Pieza","Descripcion":"Producto de prueba","ValorUnitario":"1000.00","Importe":"1000.00","Descuento":null,"ObjetoImp":"02","Impuestos":{"Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}],"Retenciones":[]}}],"Impuestos":{"TotalImpuestosTrasladados":"160.00","Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}]},"CfdiRelacionados":null,"Complemento":null,"InformacionGlobal":null}}' - Descarga el XML y el PDF. Con el UUID de la respuesta: GET
/cfdis/{uuid}/xmly/cfdis/{uuid}/pdf. - Pasa a producción. Cuando todo funcione, crea una cuenta en
https://v2.facter.com.mx, genera ahí tu key de producción y cambia en tu integración solo dos cosas: labase_urlahttps://v2.facter.com.mx/api/ext/v1y la API key. El resto del código es idéntico.
base_url a producción cuando estés listo.Cómo obtener acceso
- Empieza en demo. Regístrate en
https://demo.facter.com.mx, entra a Mi cuenta → API y presiona Activar. El alta es autoservicio. En demo no necesitas comprar timbres: timbras contra el sandbox de pruebas sin cobro real. - Crea tu cuenta de producción cuando estés listo. Regístrate en
https://v2.facter.com.mx(es una cuenta distinta a la de demo; ver Ambientes), activa el API y compra timbres (PREPAGO). El timbrado real exige saldo — sin saldo recibirás402 NO_STAMPS_AVAILABLE. Mínimo 25 timbres, a $4 + IVA cada uno. Los timbres prepago no caducan.Regla fiscal en la compra (emisor Facter, RESICO): si el RFC de tu cuenta es persona moral (RFC de 12 caracteres) se aplica retención ISR 1.25% además de IVA 16%; si es persona física, solo IVA 16%. - Genera tus keys desde la UI de autoservicio (Mi cuenta → API) en cada ambiente. Toda key es
fct_live_*; la de demo solo funciona contra demo y la de producción solo contra producción.
RFC y CSD de pruebas
Para el ambiente demo usa el RFC de pruebas público del SAT EKU9003173C9 (ESCUELA KEMPER URGATE) con su CSD de pruebas. Nosotros te los proporcionamos — no necesitas un PAC propio: descarga el paquete de certificados de prueba. Es el bundle oficial de RFCs de prueba del SAT (incluye EKU9003173C9 y otros, personas físicas y morales) con sus archivos .cer / .key y la contraseña en Contraseña.txt. Nunca uses CSDs reales en el ambiente demo.
Autenticación
Toda petición lleva tu API key como Bearer token:
Authorization: Bearer fct_live_xxxxxxxxxxxxxxxx
El prefijo es siempre fct_live_ y ya no codifica el comportamiento: lo que cambia es el host al que llamas (ver Ambientes).
| Ambiente | Host | Efecto |
|---|---|---|
| Demo | https://demo.facter.com.mx | Timbra contra el sandbox de pruebas, sin cobro real. Persiste en la BD demo (efímera). |
| Producción | https://v2.facter.com.mx | Timbra de verdad y consume del pool del principal. |
Scopes
Cada key tiene scopes que limitan qué puede hacer:
| Scope | Permite |
|---|---|
cfdi:stamp | Timbrar y validar. |
cfdi:read | Consultar, listar, descargar XML/PDF, estatus de cancelación. |
cfdi:cancel | Solicitar cancelaciones. |
emisores:manage | Alta de emisores y carga de CSD. |
timbres:read | Consultar saldo y reporte de consumo. |
Idempotencia
El timbrado cuesta dinero: un reintento de red no debe timbrar (ni cobrar) dos veces. Por eso los endpoints mutadores exigen el header:
Idempotency-Key: a3f1c2e4-9b8d-4c7a-8e2f-1d6b5a4c3e2f
- Genera un UUID nuevo por cada intento lógico de timbrado.
- Reintenta con la MISMA key ante timeout o 5xx: te devolvemos el mismo resultado sin volver a timbrar.
- Reusar la misma key con un cuerpo distinto responde
409 IDEMPOTENCY_CONFLICT. - Si una operación con esa key sigue en curso, responde
409 IDEMPOTENCY_IN_FLIGHT: reintenta en unos segundos.
500 nunca ocurre con un timbre ya consumido: la conciliación interna garantiza entregar el CFDI. Reintentar la consulta con la misma key lo recupera.Ambientes: demo y producción
El comportamiento lo determina el ambiente (host) al que llamas, no un flag ni el prefijo de la key. Hay dos ambientes físicamente separados (BD, almacenamiento y recursos distintos):
| Demo | Producción | |
|---|---|---|
| URL base | https://demo.facter.com.mx/api/ext/v1 | https://v2.facter.com.mx/api/ext/v1 |
| Timbrado | Sandbox de pruebas (sin validez fiscal) | Timbrado fiscal real |
| Cobro | Sin cobro real | Consume timbres del pool prepago |
| Datos | Efímeros (pueden reiniciarse) | Persistentes (system of record) |
| Cuenta y key | Propias de demo | Propias de producción |
- Necesitas una cuenta en cada ambiente. Te registras una vez en demo y otra en producción; son independientes.
- La API key de un ambiente no funciona en el otro. Genera una key en cada uno.
- Para ir de demo a producción solo cambias la
base_urly la API key en tu integración. El contrato (endpoints, JSON, errores) es idéntico.
402 NO_STAMPS_AVAILABLE. Asegúrate de manejar ese caso (y la alerta timbres.bajo_saldo) antes de producir; revísalo con tu lógica aunque no lo dispares en demo.Guía: timbrar un ingreso
POST /cfdis · scope cfdi:stamp. Envía emisor_rfc (debe coincidir con cfdi.Emisor.Rfc), una external_ref opcional y el comprobante completo en cfdi.
curl -X POST https://v2.facter.com.mx/api/ext/v1/cfdis \
-H "Authorization: Bearer fct_live_xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"emisor_rfc":"EKU9003173C9","external_ref":"REF-FAC-000123","fecha_emision":null,"cfdi":{"Version":"4.0","Serie":"A","Folio":"123","FormaPago":"01","MetodoPago":"PUE","CondicionesDePago":null,"SubTotal":"1000.00","Descuento":null,"Moneda":"MXN","Total":"1160.00","TipoDeComprobante":"I","Exportacion":"01","LugarExpedicion":"64000","Emisor":{"Rfc":"EKU9003173C9","Nombre":"ESCUELA KEMPER URGATE","RegimenFiscal":"601"},"Receptor":{"Rfc":"XAXX010101000","Nombre":"PUBLICO EN GENERAL","DomicilioFiscalReceptor":"64000","RegimenFiscalReceptor":"616","UsoCFDI":"S01"},"Conceptos":[{"ClaveProdServ":"01010101","NoIdentificacion":"SKU-01","Cantidad":"1.000000","ClaveUnidad":"H87","Unidad":"Pieza","Descripcion":"Producto de prueba","ValorUnitario":"1000.00","Importe":"1000.00","Descuento":null,"ObjetoImp":"02","Impuestos":{"Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}],"Retenciones":[]}}],"Impuestos":{"TotalImpuestosTrasladados":"160.00","Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}]},"CfdiRelacionados":null,"Complemento":null,"InformacionGlobal":null}}'<?php
$payload = {
"emisor_rfc": "EKU9003173C9",
"external_ref": "REF-FAC-000123",
"fecha_emision": null,
"cfdi": {
"Version": "4.0",
"Serie": "A",
"Folio": "123",
"FormaPago": "01",
"MetodoPago": "PUE",
"CondicionesDePago": null,
"SubTotal": "1000.00",
"Descuento": null,
"Moneda": "MXN",
"Total": "1160.00",
"TipoDeComprobante": "I",
"Exportacion": "01",
"LugarExpedicion": "64000",
"Emisor": {
"Rfc": "EKU9003173C9",
"Nombre": "ESCUELA KEMPER URGATE",
"RegimenFiscal": "601"
},
"Receptor": {
"Rfc": "XAXX010101000",
"Nombre": "PUBLICO EN GENERAL",
"DomicilioFiscalReceptor": "64000",
"RegimenFiscalReceptor": "616",
"UsoCFDI": "S01"
},
"Conceptos": [
{
"ClaveProdServ": "01010101",
"NoIdentificacion": "SKU-01",
"Cantidad": "1.000000",
"ClaveUnidad": "H87",
"Unidad": "Pieza",
"Descripcion": "Producto de prueba",
"ValorUnitario": "1000.00",
"Importe": "1000.00",
"Descuento": null,
"ObjetoImp": "02",
"Impuestos": {
"Traslados": [
{
"Base": "1000.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "160.00"
}
],
"Retenciones": []
}
}
],
"Impuestos": {
"TotalImpuestosTrasladados": "160.00",
"Traslados": [
{
"Base": "1000.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "160.00"
}
]
},
"CfdiRelacionados": null,
"Complemento": null,
"InformacionGlobal": null
}
};
$ch = curl_init("https://v2.facter.com.mx/api/ext/v1/cfdis");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("FACTER_API_KEY"),
"Idempotency-Key: " . bin2hex(random_bytes(16)),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$reqId = null;
curl_close($ch);
$res = json_decode($body, true);
if ($status === 201) {
echo "Timbrado UUID: " . $res["data"]["uuid"] . PHP_EOL;
} else {
// NUNCA reintentes un 422 (validación). Sí reintenta timeouts/5xx con la MISMA key.
echo "Error {$res['code']}: {$res['message']}" . PHP_EOL;
}using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FACTER_API_KEY"));
var json = """
{
"emisor_rfc": "EKU9003173C9",
"external_ref": "REF-FAC-000123",
"fecha_emision": null,
"cfdi": {
"Version": "4.0",
"Serie": "A",
"Folio": "123",
"FormaPago": "01",
"MetodoPago": "PUE",
"CondicionesDePago": null,
"SubTotal": "1000.00",
"Descuento": null,
"Moneda": "MXN",
"Total": "1160.00",
"TipoDeComprobante": "I",
"Exportacion": "01",
"LugarExpedicion": "64000",
"Emisor": {
"Rfc": "EKU9003173C9",
"Nombre": "ESCUELA KEMPER URGATE",
"RegimenFiscal": "601"
},
"Receptor": {
"Rfc": "XAXX010101000",
"Nombre": "PUBLICO EN GENERAL",
"DomicilioFiscalReceptor": "64000",
"RegimenFiscalReceptor": "616",
"UsoCFDI": "S01"
},
"Conceptos": [
{
"ClaveProdServ": "01010101",
"NoIdentificacion": "SKU-01",
"Cantidad": "1.000000",
"ClaveUnidad": "H87",
"Unidad": "Pieza",
"Descripcion": "Producto de prueba",
"ValorUnitario": "1000.00",
"Importe": "1000.00",
"Descuento": null,
"ObjetoImp": "02",
"Impuestos": {
"Traslados": [
{
"Base": "1000.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "160.00"
}
],
"Retenciones": []
}
}
],
"Impuestos": {
"TotalImpuestosTrasladados": "160.00",
"Traslados": [
{
"Base": "1000.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "160.00"
}
]
},
"CfdiRelacionados": null,
"Complemento": null,
"InformacionGlobal": null
}
}
""";
var req = new HttpRequestMessage(HttpMethod.Post, "https://v2.facter.com.mx/api/ext/v1/cfdis");
req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
var body = await res.Content.ReadAsStringAsync();
if ((int)res.StatusCode == 201)
Console.WriteLine("Timbrado: " + body);
else
// No reintentes 4xx; sí timeout/5xx con la MISMA key.
Console.Error.WriteLine($"Error {(int)res.StatusCode}: {body}");import { randomUUID } from "node:crypto";
const payload = {"emisor_rfc":"EKU9003173C9","external_ref":"REF-FAC-000123","fecha_emision":null,"cfdi":{"Version":"4.0","Serie":"A","Folio":"123","FormaPago":"01","MetodoPago":"PUE","CondicionesDePago":null,"SubTotal":"1000.00","Descuento":null,"Moneda":"MXN","Total":"1160.00","TipoDeComprobante":"I","Exportacion":"01","LugarExpedicion":"64000","Emisor":{"Rfc":"EKU9003173C9","Nombre":"ESCUELA KEMPER URGATE","RegimenFiscal":"601"},"Receptor":{"Rfc":"XAXX010101000","Nombre":"PUBLICO EN GENERAL","DomicilioFiscalReceptor":"64000","RegimenFiscalReceptor":"616","UsoCFDI":"S01"},"Conceptos":[{"ClaveProdServ":"01010101","NoIdentificacion":"SKU-01","Cantidad":"1.000000","ClaveUnidad":"H87","Unidad":"Pieza","Descripcion":"Producto de prueba","ValorUnitario":"1000.00","Importe":"1000.00","Descuento":null,"ObjetoImp":"02","Impuestos":{"Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}],"Retenciones":[]}}],"Impuestos":{"TotalImpuestosTrasladados":"160.00","Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}]},"CfdiRelacionados":null,"Complemento":null,"InformacionGlobal":null}};
const res = await fetch("https://v2.facter.com.mx/api/ext/v1/cfdis", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.FACTER_API_KEY}`,
"Idempotency-Key": randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const data = await res.json();
if (res.status === 201) {
console.log("Timbrado UUID:", data.data.uuid);
} else {
// No reintentes 4xx; sí reintenta timeout/5xx con la MISMA Idempotency-Key.
console.error(`Error ${data.code}: ${data.message}`);
}Respuesta 201
{
"status": "success",
"message": "CFDI timbrado correctamente",
"data": {
"uuid": "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
"total": "1160.00",
"fecha_timbrado": "2026-06-10T12:34:58",
"links": {
"xml": "/api/ext/v1/cfdis/AAAA…/xml",
"pdf": "/api/ext/v1/cfdis/AAAA…/pdf"
},
"timbres": {
"consumidos": 1,
"saldo_restante": 4987
}
}
}
Exento se envían solo con Base, Impuesto y TipoFactor (sin TasaOCuota ni Importe).Guía: validar sin timbrar (dry-run)
POST /cfdis/validate ejecuta TODO el pipeline (estructura, aritmética, catálogos SAT, reglas fiscales, CSD, saldo) sin sellar ni consumir timbre. No requiere Idempotency-Key. Útil para previsualizar errores antes de timbrar.
curl -X POST https://v2.facter.com.mx/api/ext/v1/cfdis/validate \
-H "Authorization: Bearer fct_live_xxxx" \
-H "Content-Type: application/json" \
-d '{"emisor_rfc":"EKU9003173C9","external_ref":"REF-FAC-000123","fecha_emision":null,"cfdi":{"Version":"4.0","Serie":"A","Folio":"123","FormaPago":"01","MetodoPago":"PUE","CondicionesDePago":null,"SubTotal":"1000.00","Descuento":null,"Moneda":"MXN","Total":"1160.00","TipoDeComprobante":"I","Exportacion":"01","LugarExpedicion":"64000","Emisor":{"Rfc":"EKU9003173C9","Nombre":"ESCUELA KEMPER URGATE","RegimenFiscal":"601"},"Receptor":{"Rfc":"XAXX010101000","Nombre":"PUBLICO EN GENERAL","DomicilioFiscalReceptor":"64000","RegimenFiscalReceptor":"616","UsoCFDI":"S01"},"Conceptos":[{"ClaveProdServ":"01010101","NoIdentificacion":"SKU-01","Cantidad":"1.000000","ClaveUnidad":"H87","Unidad":"Pieza","Descripcion":"Producto de prueba","ValorUnitario":"1000.00","Importe":"1000.00","Descuento":null,"ObjetoImp":"02","Impuestos":{"Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}],"Retenciones":[]}}],"Impuestos":{"TotalImpuestosTrasladados":"160.00","Traslados":[{"Base":"1000.00","Impuesto":"002","TipoFactor":"Tasa","TasaOCuota":"0.160000","Importe":"160.00"}]},"CfdiRelacionados":null,"Complemento":null,"InformacionGlobal":null}}'Respuesta 200 con data.valid, errors[] y warnings[] (los warnings, como la retención RESICO sugerida, no bloquean).
Guía: Complemento de Pagos 2.0
Para un recibo de pago usa TipoDeComprobante:"P". Reglas: sin FormaPago/MetodoPago, SubTotal:"0", Total:"0", Moneda:"XXX", un concepto fijo 84111506/ACT, y el nodo Complemento.Pagos con sus DoctosRelacionados (UUID del CFDI de ingreso, parcialidad, saldos). Cuando MonedaP/MonedaDR es MXN debes mandar TipoCambioP:"1" y EquivalenciaDR:"1" (reglas SAT CRP20215/CRP20238). El API valida multimoneda DR y saldos.
Ver ejemplo de Complemento.Pagos
{
"Version": "2.0",
"Totales": {
"MontoTotalPagos": "1160.00"
},
"Pago": [
{
"FechaPago": "2026-06-10T12:00:00",
"FormaDePagoP": "03",
"MonedaP": "MXN",
"TipoCambioP": "1",
"Monto": "1160.00",
"DoctosRelacionados": [
{
"IdDocumento": "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
"Serie": "A",
"Folio": "123",
"MonedaDR": "MXN",
"EquivalenciaDR": "1",
"NumParcialidad": "1",
"ImpSaldoAnt": "1160.00",
"ImpPagado": "1160.00",
"ImpSaldoInsoluto": "0.00",
"ObjetoImpDR": "01"
}
]
}
]
}
Guía: Notas de crédito (Egreso)
Usa TipoDeComprobante:"E" con el nodo CfdiRelacionados (TipoRelacion 01–07) apuntando al UUID de la factura origen. El tipo 07 (aplicación de anticipo) exige FormaPago:"30".
Guía: cancelación (asíncrona)
POST /cfdis/{uuid}/cancelacion con motivo del catálogo SAT (el 01 exige folio_sustitucion_uuid). Responde 202 Accepted: la cancelación SAT es asíncrona y puede tardar hasta 72 horas hábiles cuando requiere aceptación del receptor.
Da seguimiento con GET /cfdis/{uuid}/cancelacion (estados SOLICITADO_201, EN_PROCESO_ACEPTACION, CANCELADO, RECHAZADO) o, mejor, suscríbete al webhook cfdi.cancelado / cfdi.cancelacion_rechazada.
Guía: emisores y CSD
Da de alta cada empresa emisora con POST /emisores (transaccional: crea el tenant, lo vincula y lo agrega al pool). Luego carga su CSD con PUT /emisores/{rfc}/csd.
No necesitas POST /emisores para tu RFC propio: tu cuenta principal ya está dada de alta como emisor automáticamente (is_principal: true en GET /emisores). Solo necesitas tu CSD, que configuras como siempre en Mi Empresa (o por PUT /emisores/{tu_rfc}/csd).
Validaciones del CSD (sin excepción): que sea CSD y no FIEL, vigencia, correspondencia cert/key y que el RFC del certificado coincida con el del emisor. Recibirás el webhook emisor.csd_por_vencer antes del vencimiento.
Guía: saldo y consumo
GET /timbres devuelve el saldo del pool y el consumo del mes desglosado por emisor — la base para que el principal re-facture a sus tenants. GET /timbres/reporte?desde=&hasta= da el detalle por emisor y por día (periodo máximo 12 meses).
Webhooks
Configura tu endpoint con PUT /webhooks. Eventos disponibles: cfdi.timbrado, cfdi.cancelado, cfdi.cancelacion_rechazada, timbres.bajo_saldo, emisor.csd_por_vencer.
webhook_secret. Se genera y se muestra una sola vez al activar tus webhooks en el portal (Mi cuenta → Webhooks → Activar webhooks); guárdalo en un lugar seguro. Si lo perdiste, usa Rotar secreto. Mientras tu cuenta no tenga secret, PUT /webhooks responde 409 WEBHOOK_NOT_ACTIVATED.El payload de cfdi.timbrado incluye el campo origin ("API" | "WEB") para que distingas el canal. En CFDIs timbrados por la web, external_ref llega como null (no hay referencia externa). El campo origin es aditivo: no rompe consumidores existentes.
Cada entrega incluye el header X-Facter-Signature: sha256=<HMAC del cuerpo>. Verifica siempre la firma recomputando el HMAC-SHA256 del cuerpo crudo con tu webhook_secret, usando comparación en tiempo constante:
<?php
// Verificación de firma del webhook (HMAC-SHA256, comparación en tiempo constante).
$raw = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_FACTER_SIGNATURE"] ?? ""; // "sha256=...."
$secret = getenv("FACTER_WEBHOOK_SECRET");
$expected = "sha256=" . hash_hmac("sha256", $raw, $secret);
if (!hash_equals($expected, $header)) {
http_response_code(401);
exit("firma inválida");
}
$event = json_decode($raw, true);
// Idempotencia: deduplica por $event["data"]["uuid"] + $event["event"].
http_response_code(200); // responde 2xx rápido; procesa en segundo plano.using System.Security.Cryptography;
using System.Text;
// En tu endpoint, lee el cuerpo CRUDO (no deserializado) para el HMAC.
string raw = await new StreamReader(Request.Body).ReadToEndAsync();
string header = Request.Headers["X-Facter-Signature"]; // "sha256=...."
string secret = Environment.GetEnvironmentVariable("FACTER_WEBHOOK_SECRET")!;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
string expected = "sha256=" +
Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(raw))).ToLowerInvariant();
bool ok = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(header));
if (!ok) return Unauthorized("firma inválida");
// Deduplica por data.uuid + event. Responde 200 rápido.import crypto from "node:crypto";
import express from "express";
const app = express();
// Necesitas el cuerpo CRUDO para el HMAC:
app.use("/facter/webhook", express.raw({ type: "*/*" }));
app.post("/facter/webhook", (req, res) => {
const sig = req.get("X-Facter-Signature") || "";
const expected = "sha256=" +
crypto.createHmac("sha256", process.env.FACTER_WEBHOOK_SECRET)
.update(req.body) // Buffer crudo
.digest("hex");
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(401).send("firma inválida");
const event = JSON.parse(req.body.toString("utf8"));
// Deduplica por event.data.uuid + event.event.
res.sendStatus(200);
});# El webhook llega como POST a tu URL con el header:
# X-Facter-Signature: sha256=<hmac>
# Verifica recomputando el HMAC del cuerpo CRUDO con tu webhook_secret.- Reintentos: backoff 1m → 5m → 30m → 2h → 12h; tras agotarse, va a DLQ + alerta admin.
- Entrega at-least-once: puedes recibir el mismo evento más de una vez. Deduplica por
data.uuid+event. - Responde
2xxrápido y procesa en segundo plano. Tu endpoint debe ser HTTPS (bloqueamos IPs privadas — anti-SSRF).
Ejemplo de payload cfdi.cancelado
{
"event": "cfdi.cancelado",
"occurred_at": "2026-06-12T09:00:00-06:00",
"data": {
"uuid": "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
"emisor_rfc": "EKU9003173C9",
"external_ref": "REF-FAC-000123",
"cancel_status": "CANCELADO"
}
}
Ejemplo de payload cfdi.timbrado (timbrado desde la web)
{
"event": "cfdi.timbrado",
"occurred_at": "2026-06-16T09:00:00-06:00",
"data": {
"uuid": "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
"emisor_rfc": "EKU9003173C9",
"external_ref": null,
"serie": "F",
"folio": "1024",
"total": "1160.00",
"origin": "WEB"
}
}
Ejemplo de payload emisor.csd_por_vencer
{
"event": "emisor.csd_por_vencer",
"occurred_at": "2026-06-16T09:00:00-06:00",
"data": {
"emisor_rfc": "EKU9003173C9",
"external_ref": "REF-EMISOR-001",
"valid_to": "2026-08-15",
"dias_restantes": 15,
"etapa": "15d"
}
}
valid_to es la fecha de vencimiento del CSD en formato YYYY-MM-DD (solo fecha, sin hora). etapa indica qué umbral se cruzó: 3m, 1m, 15d, 3d o expired.
Manejo de errores
Los errores usan el envelope {status, message, code, errors?}. El code es estable (machine-readable); message es legible en español.
| HTTP | code | Qué hacer |
|---|---|---|
| 400 | INVALID_PAYLOAD | JSON malformado / campos faltantes. Revisa errors[]. No reintentar igual. |
| 401 | INVALID_API_KEY | Key inexistente, revocada o expirada. Verifica el header. |
| 402 | NO_STAMPS_AVAILABLE | Pool sin timbres. Recarga; el mensaje incluye saldo. |
| 403 | EMISOR_NOT_OWNED · EMISOR_SUSPENDED · INSUFFICIENT_SCOPE · IP_NOT_ALLOWED · CLIENT_SUSPENDED | Problema de permisos/pertenencia. No reintentar sin corregir. El RFC de tu cuenta principal queda ligado automáticamente al activar tu API; si lo ves con tu propio RFC, genera (o regenera) una API key para forzar el alta. |
| 404 | CFDI_NOT_FOUND | El UUID no pertenece a un emisor tuyo (no se revela su existencia). |
| 409 | IDEMPOTENCY_CONFLICT · IDEMPOTENCY_IN_FLIGHT | Conflicto: usa una key nueva por intento; si está en curso, reintenta en segundos. |
| 409 | WEBHOOK_NOT_ACTIVATED | Solo en PUT /webhooks: tu cuenta aún no tiene webhook_secret. Actívalo en el portal (Mi cuenta → Webhooks → Activar webhooks; se muestra una sola vez) y reintenta. |
| 413 | PAYLOAD_TOO_LARGE | El cuerpo excede 1 MB. Reduce el comprobante. |
| 422 | FISCAL_VALIDATION_FAILED · CSD_INVALID · PAC_REJECTED | Nunca reintentar igual. Corrige según errors[] (código por regla). |
| 429 | RATE_LIMITED | Respeta el header Retry-After antes de reintentar. |
| 500 | INTERNAL_ERROR | Reintenta idempotente. Reporta con tu X-Request-Id. |
422 con el mismo cuerpo; siempre reintenta un timeout/5xx de forma idempotente; respeta Retry-After en 429; guarda el X-Request-Id en tus logs para soporte.Patrón de retry seguro
Genera la Idempotency-Key una vez por intento lógico y reintenta con backoff exponencial solo ante timeout/5xx, siempre con la misma key:
import { randomUUID } from "node:crypto";
// Genera la Idempotency-Key UNA vez por intento lógico de timbrado.
// Reintenta SOLO ante timeout / 5xx, SIEMPRE con la misma key.
async function stampWithRetry(payload, apiKey, maxRetries = 4) {
const key = randomUUID();
let delay = 1000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch("https://v2.facter.com.mx/api/ext/v1/cfdis", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Idempotency-Key": key, // ← MISMA key en cada reintento
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (res.status < 500) return res; // 2xx/4xx → resultado definitivo, NO reintentar
} catch (_) { /* error de red → reintentar */ }
await new Promise(r => setTimeout(r, delay));
delay *= 2; // backoff exponencial
}
throw new Error("timbrado no confirmado tras reintentos");
}Límites y políticas
| Límite | Valor |
|---|---|
| Rate-limit por API key | Configurable por cliente (por minuto). |
| Rate-limit por IP | 300/min. |
| Tamaño de payload (mutadores) | 1 MB → 413 PAYLOAD_TOO_LARGE. |
| Allowlist de IPs por cliente | Opcional → 403 IP_NOT_ALLOWED. |
Ventana de fecha_emision | Hasta 72 h hacia atrás, nunca futura. |
| Periodo máx. de reporte | 12 meses. |
Versionado y deprecación
- La versión va en la ruta (
/v1/). Cambios incompatibles →/v2/. - Agregar campos a las respuestas NO es breaking: tu cliente debe tolerar campos desconocidos.
- Las deprecaciones se anuncian con header
Sunseten las respuestas afectadas.
Checklist de paso a producción
Antes de pasar de tu cuenta demo a producción (cambiar base_url + API key), confirma:
- ☐ Flujo de timbrado probado en el ambiente demo (ingreso, pagos, NC).
- ☐ Manejo de errores implementado, en especial
422(sin reintento) y el patrón de retry idempotente. - ☐ Cancelación probada de extremo a extremo (solicitud + seguimiento por webhook).
- ☐ Verificación de firma de webhooks funcionando y deduplicación por
uuid+event. - ☐ Manejo de
NO_STAMPS_AVAILABLEy de la alertatimbres.bajo_saldo. - ☐ CSDs reales cargados para cada emisor de producción.
- ☐ Almacenas el
X-Request-Idde cada respuesta en tus logs.
Preguntas frecuentes
¿Facter recalcula mis totales e impuestos?
No. Tú los calculas; Facter valida consistencia aritmética y contra catálogos SAT, pero tu sistema es el sistema de registro. Un descuadre devuelve FISCAL_VALIDATION_FAILED con el detalle.
¿Quién paga los timbres si tengo varios emisores?
El cliente principal. Todos los emisores consumen del mismo pool de timbres prepago del principal.
¿Puedo timbrar con el RFC de mi cuenta principal?
Sí, sin pasos extra: tu cuenta principal queda registrada como emisor automáticamente al activar tu API. Usa tu propio RFC en emisor_rfc; consume tu saldo PREPAGO directamente (la misma bolsa del pool). En GET /emisores lo identificas por is_principal: true. Si tu cuenta es anterior a esta mejora y recibes EMISOR_NOT_OWNED con tu propio RFC, genera (o regenera) una API key para forzar el alta.
¿Por qué un reintento no me cobró otro timbre?
Por la idempotencia: reintentar con la misma Idempotency-Key devuelve el resultado original sin volver a timbrar.
Recibí un 500, ¿se consumió mi timbre?
Si el timbre se consumió, la respuesta nunca es 500: la conciliación garantiza entregarte el CFDI. Reintenta la consulta con la misma key.
¿Cuánto tarda una cancelación?
Es asíncrona. Cuando requiere aceptación del receptor puede tardar hasta 72 horas hábiles. Suscríbete al webhook cfdi.cancelado.
¿Puedo leer los CFDIs que timbré desde la UI de Facter?
Sí. GET /cfdis devuelve todos los CFDIs de tus emisores, incluidos los de origen UI.
¿Qué RFC uso para pruebas?
EKU9003173C9 con el CSD de pruebas del SAT, en el ambiente demo. Nunca uses CSDs reales en demo.
¿Necesito una cuenta distinta para demo y para producción?
Sí. Son ambientes físicamente separados: te registras en https://demo.facter.com.mx para probar y en https://v2.facter.com.mx para producir. La API key de uno no funciona en el otro; para pasar a producción cambias base_url y key, nada más.
¿Mi key sirve para emisores de otro cliente?
No. Cada key solo puede operar con los emisores de su propio cliente. Intentarlo devuelve EMISOR_NOT_OWNED.
Soporte
Para integradores: soporte@facter.com.mx. Para que podamos ayudarte rápido, incluye en tu reporte:
- El prefijo de tu key (p. ej.
fct_live_) y el ambiente (demo o producción) — nunca la key completa. - El
X-Request-Idde la respuesta. - La fecha/hora (timestamp) del intento.