Facter Developers Referencia API ↗

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.

Cliente principal (PREPAGO, dueño de API keys) │ compra timbres ──► Pool de timbres compartido │ ▲ ▲ ▲ ├── Emisor A (RFC + CSD) ────┘ │ │ consume 1 timbre por CFDI ├── Emisor B (RFC + CSD) ───────────┘ │ ├── Emisor C (RFC + CSD) ──────────────────┘ └── El propio Principal (su RFC + CSD, is_principal) ── consume el mismo saldo, sin pool
Tu sistema es el sistema de registro (la fuente de verdad del comprobante): tú calculas totales e impuestos. Facter valida la consistencia (aritmética + catálogos SAT + reglas fiscales) pero no recalcula nada.

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.

  1. 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 key fct_live_*. En demo el timbrado usa el sandbox de pruebas sin cobro real.
  2. 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"}'
  3. Sube el CSD de pruebas del emisor (archivos .cer y .key en base64 + contraseña) con PUT /emisores/EKU9003173C9/csd.
  4. 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}}'
  5. Descarga el XML y el PDF. Con el UUID de la respuesta: GET /cfdis/{uuid}/xml y /cfdis/{uuid}/pdf.
  6. 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: la base_url a https://v2.facter.com.mx/api/ext/v1 y la API key. El resto del código es idéntico.
¿Prefieres Postman? Descarga la colección oficial: por defecto apunta al ambiente demo; cambia la variable base_url a producción cuando estés listo.

Cómo obtener acceso

  1. 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.
  2. 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ás 402 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%.
  3. 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).

AmbienteHostEfecto
Demohttps://demo.facter.com.mxTimbra contra el sandbox de pruebas, sin cobro real. Persiste en la BD demo (efímera).
Producciónhttps://v2.facter.com.mxTimbra de verdad y consume del pool del principal.
Una API key solo funciona en el ambiente donde se emitió: la key de demo no sirve en producción ni viceversa.

Scopes

Cada key tiene scopes que limitan qué puede hacer:

ScopePermite
cfdi:stampTimbrar y validar.
cfdi:readConsultar, listar, descargar XML/PDF, estatus de cancelación.
cfdi:cancelSolicitar cancelaciones.
emisores:manageAlta de emisores y carga de CSD.
timbres:readConsultar saldo y reporte de consumo.
Nunca expongas tu key en el frontend. Se guarda hasheada en reposo; trátala como secreto. Si se filtra, revócala y rota desde Mi cuenta → API.

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.
El 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):

DemoProducción
URL basehttps://demo.facter.com.mx/api/ext/v1https://v2.facter.com.mx/api/ext/v1
TimbradoSandbox de pruebas (sin validez fiscal)Timbrado fiscal real
CobroSin cobro realConsume timbres del pool prepago
DatosEfímeros (pueden reiniciarse)Persistentes (system of record)
Cuenta y keyPropias de demoPropias 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_url y la API key en tu integración. El contrato (endpoints, JSON, errores) es idéntico.
Diferencia conocida: demo opera con un pool de timbres holgado, así que normalmente no reproduce 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
        }
    }
}
Los impuestos 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 0107) 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.

Antes de configurar necesitas tu 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.
Los webhooks no están atados solo al API. Los eventos se disparan para los CFDIs de tu cuenta sin importar el canal: tanto los timbrados/cancelados por este API como los emitidos desde la operación web de Facter (facturas, notas de crédito, pagos, nómina). Una sola configuración de webhooks cubre ambos orígenes. Requisito mínimo para recibirlos: tener el API activado en autoservicio (se activa solo al guardar tu primer webhook desde Webhooks en el menú) y un webhook suscrito al evento; no se exige plan PREPAGO para configurar (timbrar sí requiere saldo).

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 2xx rá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.

HTTPcodeQué hacer
400INVALID_PAYLOADJSON malformado / campos faltantes. Revisa errors[]. No reintentar igual.
401INVALID_API_KEYKey inexistente, revocada o expirada. Verifica el header.
402NO_STAMPS_AVAILABLEPool sin timbres. Recarga; el mensaje incluye saldo.
403EMISOR_NOT_OWNED · EMISOR_SUSPENDED · INSUFFICIENT_SCOPE · IP_NOT_ALLOWED · CLIENT_SUSPENDEDProblema 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.
404CFDI_NOT_FOUNDEl UUID no pertenece a un emisor tuyo (no se revela su existencia).
409IDEMPOTENCY_CONFLICT · IDEMPOTENCY_IN_FLIGHTConflicto: usa una key nueva por intento; si está en curso, reintenta en segundos.
409WEBHOOK_NOT_ACTIVATEDSolo 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.
413PAYLOAD_TOO_LARGEEl cuerpo excede 1 MB. Reduce el comprobante.
422FISCAL_VALIDATION_FAILED · CSD_INVALID · PAC_REJECTEDNunca reintentar igual. Corrige según errors[] (código por regla).
429RATE_LIMITEDRespeta el header Retry-After antes de reintentar.
500INTERNAL_ERRORReintenta idempotente. Reporta con tu X-Request-Id.
Regla de oro: nunca reintentes un 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ímiteValor
Rate-limit por API keyConfigurable por cliente (por minuto).
Rate-limit por IP300/min.
Tamaño de payload (mutadores)1 MB → 413 PAYLOAD_TOO_LARGE.
Allowlist de IPs por clienteOpcional → 403 IP_NOT_ALLOWED.
Ventana de fecha_emisionHasta 72 h hacia atrás, nunca futura.
Periodo máx. de reporte12 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 Sunset en 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_AVAILABLE y de la alerta timbres.bajo_saldo.
  • ☐ CSDs reales cargados para cada emisor de producción.
  • ☐ Almacenas el X-Request-Id de 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-Id de la respuesta.
  • La fecha/hora (timestamp) del intento.