Soluciones · Empresas con ERP, sistema propio o asistentes de IA

API REST · Webhooks · MCP · Sandbox gratuito

Timbra CFDI desde tu sistema o tu asistente de IA

La API de SenHub emite facturas, nómina, complementos de pago y retenciones CFDI 4.0 desde tu ERP o aplicación, con webhooks para cada evento y un MCP oficial para facturar desde Claude o ChatGPT. Empiezas en sandbox sin costo, pagas solo por timbre exitoso y, si operas varios RFC, lo conectas con el plan Despacho. Timbrado a través de PAC autorizado por el SAT.

¿Integración a la medida (conciliación bancaria, Odoo, SAP, CONTPAQi)? Haz el diagnóstico →

5 timbres gratis Sandbox ilimitado Integración en 5 min 99.9% uptime
POST /timbrado/timbrar
curl -X POST https://lite.senhub.mx/api/v1/timbrado/timbrar \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "serie": "A", "folio": "1001", "emisor": { "rfc": "XAXX010101000" }, "receptor":{ "rfc": "XEXX010101000", "uso_cfdi": "G03" }, "conceptos": [{ "clave": "81112100", "cantidad": 1 }] }'
201{"uuid": "9F3C21A8-…", "status": "timbrado", "ms": 312}

Planes y Precios

Elige el plan que se adapte a tu volumen. Sin contratos, cancela cuando quieras.

Dos cargos independientes

Sin suscripción aplica el precio por volumen sobre la cantidad completa. Con plan activo aplica la tarifa del plan a cualquier cantidad. Los folios se compran siempre por separado de la renta. Los folios se compran por separado, siempre por prepago. Ningún plan incluye folios y no existe postpago.

Tarifa por volumen

El total de tu compra define el precio: todos los timbres se cobran a esa tarifa, sin importar el tramo por el que pasaste.

$0.95
1 – 99 timbres
$0.90
100 – 499 timbres
$0.80
500+ timbres

Precios netos + IVA (16%). La mejor tarifa por volumen es $0.80 por timbre.

Con suscripción activa

Sin suscripción aplica el precio por volumen sobre la cantidad completa. Con plan activo aplica la tarifa del plan a cualquier cantidad. Los folios se compran siempre por separado de la renta.

  • Starter$0.75/timbrela suscripción se recupera desde ~1,980 timbres/mes
  • Pro$0.65/timbrela suscripción se recupera desde ~1,327 timbres/mes
  • Enterprise$0.60/timbrela suscripción se recupera desde ~1,995 timbres/mes

Autenticación

Autenticación por OTP (código por email). Recibes un access_token de 1 hora y un refresh_token de 30 días, que se rota en cada renovación, para seguir sin volver a pedir OTP.

Flujo de autenticación

POST /otp/requestCódigo al emailPOST /otp/verifyaccess_token + refresh_token

El OTP solo se usa una vez. Después, tu backend renueva con el refresh_token automáticamente.

1. Solicitar OTPbash
curl -X POST https://lite.senhub.mx/api/v1/auth/otp/request \
  -H "Content-Type: application/json" \
  -d '{"email": "dev@tuapp.com"}'

# → {"sent": true, "message": "Código enviado"}
2. Verificar OTP → obtener tokensbash
curl -X POST https://lite.senhub.mx/api/v1/auth/otp/verify \
  -H "Content-Type: application/json" \
  -d '{"email": "dev@tuapp.com", "code": "123456"}'

# → {
#   "access_token": "eyJhbGciOiJIUzI1NiIs...",
#   "refresh_token": "v4n...",
#   "token_type": "bearer",
#   "expires_in": 3600,
#   "tenant_id": "f1a2...",
#   "email": "dev@tuapp.com"
# }
3. Renovar token (antes de que expire, 1 h)bash
curl -X POST https://lite.senhub.mx/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "v4n..."}'

# → {"access_token": "eyJ...(nuevo)...", "refresh_token": "(nuevo: reemplaza al anterior)", "expires_in": 3600}
# Si responde 401, vuelve a pedir OTP.
4. Usar en todas las requestsbash
curl https://lite.senhub.mx/api/v1/creditos/balance \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Para tu backend: el refresh_token se rota: guarda siempre el nuevo que devuelve cada renovación y descarta el anterior. Renueva el access_token antes de que expire (1 h). Si la renovación responde 401 (refresh vencido a los 30 días o ya usado), necesitarás un nuevo OTP. Para integraciones server-to-server usa mejor una API key.

API Keys

Autenticación server-to-server sin OTP: ideal para cron jobs, backends y ERPs. Disponibles en todos los planes, incluido Gratis; el plan solo define cuántas llaves activas puedes tener.

TipoUsoAlcance
sk_live_…Producción, server-to-serverAcceso completo a la API (timbrado real).
sk_test_…Pruebas de integraciónSolo sandbox: nunca cobra folios y no puede cancelar CFDI reales (403).
pk_live_…Publicable (widgets, frontend)Solo consulta de catálogos SAT (/catalogos/*); cualquier otra ruta responde 403.
Usar una API keybash
curl https://lite.senhub.mx/api/v1/creditos/balance   -H "Authorization: Bearer sk_live_abc123..."
POST
/api-keys

Crear llave ({ name, key_type }); raw_key se muestra UNA sola vez

GET
/api-keys?status=all|active|revoked

Listar (por defecto incluye las revocadas)

GET
/api-keys/quota

Cupo usado y disponible (live y prueba)

DELETE
/api-keys/{id}

Revocar (libera el cupo al instante)

DELETE
/api-keys/{id}?permanent=true

Eliminar del historial una llave ya revocada

Cupo disponiblebash
curl https://lite.senhub.mx/api/v1/api-keys/quota   -H "Authorization: Bearer $TOKEN"

# → {
#   "plan": "starter",
#   "live": { "limit": 5, "active": 3, "remaining": 2 },   # sk_live + pk_live
#   "test": { "limit": 5, "active": 0, "remaining": 5 },   # sk_test (sandbox)
#   "revoked": 2                                            # historial; no consume cupo
# }
PlanLive (sk_live + pk_live)Prueba (sk_test)
Free13
Starter55
Pro2010
Enterprise10050

Capacidades por plan

CapacidadFreeStarterProEnterprise
Timbrado, API y sandbox (JWT o API key)SíSíSíSí
Llaves live / prueba activas1 / 35 / 520 / 10100 / 50
Clientes y productos (customer_id / product_id)NoSíSíSí
Webhooks salientesNoNoSíSí
Timbrado masivo (batch)NoNoSíSí
PDF con marca (plantilla, color, logo)NoNoSíSí
Multi-organización, equipos y roles; SLANoNoNoSí
Recibos y autofacturaSíSíSíSí

Un plan inferior que usa una función con restricción recibe 403 con el plan mínimo requerido. La renta no incluye folios: se compran aparte y el precio por folio depende del plan (ver Planes y Precios). Suscripción: GET /suscripciones/planes (público), GET /suscripciones/plan, POST /suscripciones/checkout (Stripe Checkout; el servidor define los importes) y POST /suscripciones/portal (cambiar de plan, método de pago o cancelar).

Cuenta solo las activas

GET /api-keys incluye las llaves revocadas (historial). No lo uses para contar el cupo: usa /api-keys/quota o filtra ?status=active. Cada DELETE responde además con la quota actualizada. Al llegar al límite, POST /api-keys responde 400 indicando el tipo de cupo.

Seguridad

  • Solo guardamos el hash SHA-256; la llave completa no se puede recuperar.
  • Gestionar llaves requiere sesión (JWT): crear, listar, consultar el cupo o revocar con una API key responde 403, así una llave filtrada no puede acuñar otras.
  • En organizaciones, el permiso api_keys es de owner y admin.
  • Eliminar una llave activa responde 409: primero se revoca.
  • last_used_at se actualiza como máximo una vez por minuto.

Certificados (CSD)

Sube los CSD de tus clientes y timbra para cualquiera de ellos.

RFCs por plan: Free y Starter pueden registrar 1 RFC; Pro o superior, RFCs ilimitados. Renovar un RFC ya registrado siempre está permitido y el límite nunca afecta el timbrado.

POST
/csd

Subir CSD (archivos .cer + .key + password)

GET
/csd

Listar CSD activos

DELETE
/csd/{rfc}

Eliminar CSD

Subir CSDbash
curl -X POST https://lite.senhub.mx/api/v1/csd \
  -H "Authorization: Bearer $TOKEN" \
  -F "cer=@certificado.cer" \
  -F "key=@llave.key" \
  -F "password=MiPassword123"

Qué valida el alta

  • Responde 400 si el archivo es una FIEL (no un CSD), está vencido, aún no es vigente o la contraseña no abre la llave.
  • is_recently_issued: true indica que el CSD tiene menos de 72 h de vigencia. El SAT puede tardar ese plazo en darlo de alta; mientras tanto el timbrado puede fallar con un rechazo PAC_ERROR CSD_NOT_PROPAGATED (reintentable, sin cobro). No es un error de integración.
  • Un mismo RFC puede estar activo en más de una cuenta (despachos, sucursales). La respuesta incluye shared_rfc y shared_active_tenants para que tu interfaz lo advierta. Los RFC de prueba del SAT nunca se marcan.
  • Mover un RFC entre cuentas = volver a subirlo en la cuenta destino y darlo de baja en la origen; el material se cifra por cuenta y no se puede "mover".

Timbrado

Timbra cualquier tipo de CFDI. Cada timbrado consume 1 folio y, si falla, se reembolsa automáticamente. Cancelar no consume folios.

POST
/timbrado/timbrar

Factura (I/E)

1 folio
POST
/timbrado/nomina

Nómina 1.2 (N)

1 folio
POST
/timbrado/pago

Pago REP 2.0 (P)

1 folio
POST
/timbrado/retencion

Retención 2.0 (R)

1 folio
POST
/timbrado/cancelar

Cancelar CFDI

Sin costo
POST
/timbrado/batch

Timbrado masivo (hasta 500) · Pro+

1/timbre

No envíes el campo fecha. Lite la genera al timbrar en la zona fiscal correspondiente al lugar_expedicion. Envía siempre el código postal real del lugar de emisión registrado para el emisor.

Timbrar Factura (Ingreso)bash
curl -X POST https://lite.senhub.mx/api/v1/timbrado/timbrar \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <uuid-unico-por-factura>" \
  -d '{
    "emisor_rfc": "TU_RFC_AQUI",
    "emisor_nombre": "Tu Empresa SA de CV",
    "emisor_regimen": "601",
    "lugar_expedicion": "06600",
    "tipo_de_comprobante": "I",
    "moneda": "MXN",
    "tipo_cambio": "1",
    "exportacion": "01",
    "forma_pago": "03",
    "metodo_pago": "PUE",
    "receptor": {
      "rfc": "EKU9003173C9",
      "nombre": "ESCUELA KEMPER URGATE",
      "domicilio_fiscal_receptor": "06600",
      "regimen_fiscal_receptor": "601",
      "uso_cfdi": "G03"
    },
    "conceptos": [{
      "clave_prod_serv": "84111506",
      "cantidad": 1,
      "clave_unidad": "E48",
      "descripcion": "Servicio de consultoría",
      "valor_unitario": 1000.00,
      "objeto_imp": "02",
      "traslados": [{
        "impuesto": "002",
        "tipo_factor": "Tasa",
        "tasa_o_cuota": "0.160000"
      }]
    }]
  }'

Idempotencia y campos que suelen causar rechazo

Genera un UUID como Idempotency-Key para cada factura y reutiliza exactamente la misma llave al reintentar esa operación. Así evitas un doble timbrado por timeouts o errores de red.

  • lugar_expedicion — CP del emisor. Obligatorio; define la zona horaria del comprobante.
  • exportacion — "01" para operaciones nacionales. Obligatorio en CFDI 4.0.
  • El receptor usa domicilio_fiscal_receptor y regimen_fiscal_receptor, no domicilio_fiscal ni regimen_fiscal.
  • En traslados no envíes base: el servidor la calcula desde cantidad × valor_unitario, restando el descuento del concepto si lo hay.
  • objeto_imp — "02" si el concepto causa impuestos, "01" si no es objeto de impuesto. Con "01" el concepto va sin nodo de impuestos.
  • tipo_cambio — "1" cuando la moneda es MXN; el tipo de cambio real si es otra divisa.
  • clave_prod_serv y clave_unidad deben existir en el catálogo del SAT. Resuélvelas con la búsqueda de catálogos en lugar de fijarlas a mano.

Facturar a un cliente en el extranjero

Para un receptor que no tiene RFC mexicano, el SAT usa el RFC genérico XEXX010101000. El resto del request es el mismo de arriba; cambian los datos del receptor y, normalmente, la moneda.

POST /timbrado/timbrar — receptor extranjerojson
{
  "emisor_rfc": "TU_RFC_AQUI",
  "emisor_nombre": "Tu Empresa SA de CV",
  "emisor_regimen": "601",
  "lugar_expedicion": "06600",
  "tipo_de_comprobante": "I",
  "moneda": "USD",
  "tipo_cambio": "17.2500",
  "exportacion": "01",
  "forma_pago": "03",
  "metodo_pago": "PUE",
  "receptor": {
    "rfc": "XEXX010101000",
    "nombre": "ACME INC",
    "domicilio_fiscal_receptor": "06600",
    "regimen_fiscal_receptor": "616",
    "uso_cfdi": "S01"
  },
  "conceptos": [{
    "clave_prod_serv": "84111506",
    "cantidad": 1,
    "clave_unidad": "E48",
    "descripcion": "Servicio de consultoría",
    "valor_unitario": 1000.00,
    "objeto_imp": "02",
    "traslados": [{
      "impuesto": "002",
      "tipo_factor": "Tasa",
      "tasa_o_cuota": "0.000000"
    }]
  }]
}
  • rfc — "XEXX010101000". En nombre va el nombre o razón social real del cliente, como aparece en su identificación fiscal.
  • domicilio_fiscal_receptor — con el RFC genérico extranjero el SAT exige el mismo código postal que lugar_expedicion, no el del cliente.
  • regimen_fiscal_receptor — "616" (Sin obligaciones fiscales) y uso_cfdi — "S01" (Sin efectos fiscales).
  • moneda / tipo_cambio — si facturas en otra divisa (por ejemplo USD) envía el tipo de cambio real; con MXN va "1".
  • exportacion y la tasa de IVA dependen de tu operación (servicios, mercancías, tasa 0 % o 16 %). El ejemplo es solo ilustrativo: confirma el tratamiento con tu contador.
  • residencia_fiscal y num_reg_id_trib (opcionales) — país de residencia (clave ISO alfa-3 de c_Pais, nunca MEX) y Tax ID del cliente. Solo se permiten con XEXX010101000; num_reg_id_trib exige residencia_fiscal y cumple el formato del país: USA y CAN, 9 dígitos sin guiones (un EIN 12-3456789 se escribe 123456789). El formato por país está en GET /catalogos/pais. Las exportaciones de mercancías con exportacion: "02" requieren el complemento de Comercio Exterior.

Para constancias de retención a residentes en el extranjero, consulta el receptor Extranjero en Retenciones 2.0.

Timbrado simplificado con customer_id / product_idStarter+

Si registraste tus receptores en Clientes y tus conceptos en Productos, puedes timbrar con IDs en lugar de repetir el receptor y los conceptos completos. Es 100% opcional y retrocompatible: si no envías IDs, el timbrado funciona exactamente igual que arriba.

Timbrar con IDs del catálogojson
{
  "emisor_rfc": "AAA010101AAA",
  "emisor_nombre": "EMPRESA SA DE CV",
  "emisor_regimen": "601",
  "lugar_expedicion": "06000",
  "forma_pago": "01",
  "metodo_pago": "PUE",
  "customer_id": "id-del-cliente",
  "conceptos": [
    { "product_id": "id-del-producto", "cantidad": 3 },
    { "product_id": "id-otro-producto", "cantidad": 1, "descuento": 50.0 }
  ]
}
  • customer_id → el receptor se resuelve del catálogo de Customers.
  • product_id → cada concepto (con sus impuestos) se resuelve del catálogo de Products.
  • Puedes mezclar conceptos con product_id y conceptos inline completos. Si envías el ID y campos inline, los campos inline prevalecen como override.

Factura global (público en general). Si usas el RFC genérico XAXX010101000, el SAT exige además el nodo informacion_global con periodicidad, meses y ano (regla 2.7.1.22 RMF). Sin él el comprobante se rechaza con CFDI40130.

Descuentos por concepto

El descuento se declara por concepto, con el campo opcional descuento dentro de cada elemento de conceptos. Es un importe decimal en la moneda del comprobante, no un porcentaje.

Concepto con descuentojson
"conceptos": [
  {
    "clave_prod_serv": "01010101",
    "cantidad": 1,
    "clave_unidad": "H87",
    "descripcion": "Producto con descuento",
    "valor_unitario": 3313.80,
    "descuento": 926.66,
    "objeto_imp": "02",
    "traslados": [
      { "impuesto": "002", "tipo_factor": "Tasa", "tasa_o_cuota": "0.000000" }
    ]
  }
]

El resto se calcula solo. No hay que enviarlo:

Atributo del CFDICómo se obtiene
Concepto@Importecantidad × valor_unitario
Traslado@BaseImporte − Descuento (la base se calcula después del descuento)
Comprobante@SubTotalSuma de Importe de los conceptos, antes del descuento
Comprobante@DescuentoSuma de los descuento de los conceptos
Comprobante@TotalSubTotal − Descuento + traslados − retenciones

No existe un campo descuento a nivel comprobante. Si el descuento es global, repártelo entre los conceptos: la suma se refleja automáticamente en Comprobante@Descuento.

La misma base para todos los impuestos

traslados y retenciones comparten la misma base del concepto, siempre calculada después del descuento:

Base = (cantidad × valor_unitario) − descuento
Casotipo_factorResultado
IVA 16 %, 8 %, 0 %TasaImporte = Base × TasaOCuota
IEPS y varios trasladosTasaTodos sobre la misma Base
Retención de ISR e IVATasaSobre la misma Base; suman en TotalImpuestosRetenidos
ExentoExentoLleva Base, sin Importe, no suma al total
No objeto de impuesto—objeto_imp "01" y sin nodo de impuestos
Cuota fijaCuotaRequiere enviar importe (ver aviso)

Las retenciones van en el arreglo retenciones del concepto, con la misma estructura que traslados. Se reflejan solas en Impuestos@TotalImpuestosRetenidos y se restan del total.

Retenciones en el conceptojson
"retenciones": [
  { "impuesto": "001", "tipo_factor": "Tasa", "tasa_o_cuota": "0.012500" },
  { "impuesto": "002", "tipo_factor": "Tasa", "tasa_o_cuota": "0.106667" }
]
// 001 = ISR · 002 = IVA · 003 = IEPS

Con tipo_factor: "Tasa" no hay que calcular nada. Para los casos que requieren control manual, traslados y retenciones aceptan dos campos opcionales:

  • importe — fija el importe del impuesto en lugar de calcularlo.
  • base — fija la base en lugar de usar Importe − Descuento.

Cuotas fijas (tipo_factor: "Cuota"). En el IEPS de cuota fija —combustibles, tabaco, bebidas saborizadas— el importe depende de la unidad gravada, no de la base, así que no puede derivarse automáticamente. Si no envías importe, se usa tasa_o_cuota redondeado a 2 decimales, que solo es correcto cuando la cuota aplica una vez. Para cuotas por unidad, envía importe explícitamente.

Cuota de 1.50 por unidad, 3 unidadesjson
{
  "impuesto": "003",
  "tipo_factor": "Cuota",
  "tasa_o_cuota": "1.500000",
  "importe": 4.50
}

Cifras verificadas, para cuadrar tu implementación

Todos los casos parten del mismo concepto: valor_unitario 3313.80 con descuento 926.66, es decir Base 2387.14. Compara tus números con estos.

CasoBaseImpuestoTotal
IVA 16%2387.14381.942769.08
IVA 0%2387.140.002387.14
IEPS 8% + IVA 16%2387.14190.97 + 381.942960.05
IVA 16% + retención ISR 1.25% + retención IVA 10.6667%2387.14trasladados 381.94 · retenidos 284.472484.61
Exento2387.14sin importe2387.14
No objeto de impuesto—sin importe2387.14
Cuota fija———

Contrato 2026-10-20.5 · Un concepto con descuento e IVA 0%. La base del impuesto se calcula después del descuento.

Respuesta exitosajson
{
  "id": "uuid-interno",
  "uuid": "B765E38F-1234-5678-ABCD-EF1234567890",
  "status": "stamped",
  "rfc_emisor": "ABC123456XY7",
  "rfc_receptor": "EKU9003173C9",
  "serie": "A",
  "folio": "1002",
  "total": 1160.0,
  "credits_charged": 1,
  "xml_path": "...",
  "error_code": null,
  "error_message": null,
  "sandbox": false,
  "pac_environment": "production"
}

Impuestos locales (ISH y estatales)

Los impuestos locales (p. ej. el Impuesto Sobre Hospedaje) se envían en el mismo POST /timbrado/timbrar con el campo opcional impuestos_locales (Complemento de Impuestos Locales 1.0). No hay endpoint aparte.

impuestos_localesjson
"impuestos_locales": {
  "traslados_locales": [
    { "imp_loc_trasladado": "ISH", "tasa_de_traslado": 3.0, "importe": 30.00 }
  ],
  "retenciones_locales": []
}
  • La tasa va en porcentaje (3.0 = 3 %), no en fracción. El importe es explícito (siguen reglas estatales).
  • TotaldeTraslados y TotaldeRetenciones se calculan como la suma de los importes.
  • Máximo 1 traslado local y 1 retención local por comprobante; más responde 400.
  • Se leen de vuelta en GET /timbrado/cfdi/{uuid}/detalle (campo impuestos_locales).

CFDI relacionados y redondeo de impuestos

Campo opcional cfdi_relacionados (nodo cfdi:CfdiRelacionados). Para sustituir un CFDI usa el tipo 04:

cfdi_relacionadosjson
"cfdi_relacionados": [
  { "tipo_relacion": "04", "uuid": "11111111-1111-4111-8111-111111111111" }
]
  • tipo_relacion: 01 nota de crédito · 02 nota de débito · 03 devolución · 04 sustitución · 05 traslados previos · 06 facturación por traslados · 07 anticipo · 08 pagos en parcialidades · 09 pagos diferidos.
  • uuid o uuids (lista con el mismo tipo; se eliminan duplicados y se pasan a mayúsculas). Máximo 10 grupos. Un tipo inválido o un UUID mal formado responde 422.
  • Los campos que Lite no reconoce se ignoran sin error: verifica en el XML devuelto que el nodo exista. En producción el PAC valida que el UUID relacionado exista en el SAT; el sandbox no.
  • Redondeo: el importe explícito de un impuesto se redondea con la regla del SAT (desde .5 sube: 293.645 → 293.65). Para conservar una cifra ya emitida, envía importe explícito. El IVA al 16 % no genera empates.
  • Sandbox y producción construyen el XML con el mismo código; el sandbox solo simula la respuesta del PAC.

Exportación de mercancías: Comercio Exterior 2.0

Con exportacion: "02" el comprobante exige el complemento comercio_exterior; con "01" o "03" no debe existir. Usa "01" para servicios que no son exportación definitiva. Tipos admitidos: I, E y T. Lite aplica la Matriz de Errores oficial del SAT (reglas CCE101–CCE172) sobre el XML ya construido, antes del PAC y sin cobrar; cada error trae el código de la regla.

POST /timbrado/timbrar — exportación (extracto)json
{
  "moneda": "USD", "tipo_cambio": 17.25, "exportacion": "02",
  "receptor": { "rfc": "XEXX010101000", "nombre": "ACME INC", "domicilio_fiscal_receptor": "06600",
                "regimen_fiscal_receptor": "616", "uso_cfdi": "S01",
                "residencia_fiscal": "USA", "num_reg_id_trib": "123456789" },
  "conceptos": [{ "clave_prod_serv": "50202306", "cantidad": 100, "clave_unidad": "KGM", "descripcion": "Café tostado",
                  "valor_unitario": 12.5, "no_identificacion": "SKU-1", "objeto_imp": "02",
                  "traslados": [{ "impuesto": "002", "tipo_factor": "Tasa", "tasa_o_cuota": "0.000000" }] }],
  "comercio_exterior": {
    "incoterm": "FOB",
    "receptor_domicilio": { "calle": "Main St", "numero_exterior": "100", "estado": "CA", "pais": "USA", "codigo_postal": "90001" },
    "mercancias": [{ "no_identificacion": "SKU-1", "fraccion_arancelaria": "09011101",
                     "cantidad_aduana": 100, "unidad_aduana": "01", "valor_unitario_aduana": 12.5 }]
  }
}
Campo de comercio_exteriorDescripción
clave_de_pedimentoA1 (exportación definitiva; valor por defecto). Catálogo cce-clave-pedimento.
incotermObligatorio (CCE120). Catálogo cce-incoterm.
certificado_origen / num_certificado_origen0 o 1; el número se registra si y solo si es 1 (CCE118).
numero_exportador_confiableSolo si el país del receptor o destinatario es de la Unión Europea (CCE119).
motivo_trasladoSolo traslados (T). 01 exige cfdi_relacionados (CCE116); 05 exige propietarios (CCE117).
tipo_cambio_usdPesos por dólar. Con moneda USD se toma de tipo_cambio; en otra moneda es obligatorio. Debe ser el tipo de cambio oficial del DOF: lo valida el PAC (CCE121).
total_usdOpcional: por defecto la suma de valor_dolares (debe coincidir al centavo, CCE122).
emisor{ curp, domicilio }. Con CURP obligatoria si el emisor es persona física (RFC de 13: CCE124); sin CURP si es moral (CCE123). Domicilio de México (CCE125).
receptor_domicilioObligatorio con receptor XEXX010101000 y país distinto de MEX (CCE133/CCE134); el Tax ID cumple el formato del país (CCE135).
propietarios[] / destinatarios[]propietarios: { num_reg_id_trib, residencia_fiscal } solo en traslados con motivo 05. destinatarios: { nombre, num_reg_id_trib, domicilios }; en traslados solo uno (CCE142).
mercancias[]Ver abajo.
  • Mercancías: no_identificacion debe coincidir con el de un concepto y todos los conceptos deben llevarlo (CCE151/CCE152). fraccion_arancelaria (8 dígitos vigentes, cce-fraccion-arancelaria?q=) es obligatoria salvo servicios (unidad_aduana 99 o clave_unidad E48), donde no debe registrarse.
  • cantidad_aduana, unidad_aduana y valor_unitario_aduana van los tres o ninguno, en todas las mercancías. valor_dolares por defecto es cantidad × valor unitario y debe quedar dentro de los límites de la guía del SAT y ser coherente con el importe de los conceptos convertido con el tipo de cambio.
  • Domicilios (calle, estado, pais, codigo_postal obligatorios): en México el estado es la clave c_Estado y el código postal debe existir y corresponder a estado, municipio y localidad; en USA y CAN el código postal cumple el formato del SAT (USA [0-9]{5}(-[0-9]{4})?, CAN A1A 1A1); en otros países son texto libre.
  • Coherencia: con Comercio Exterior no pueden existir informacion_global, impuestos_locales ni otros complementos (salvo Timbre, otros derechos e impuestos, leyendas fiscales, registro fiscal y Carta Porte); en ingresos el receptor no puede ser XAXX010101000; en traslados el receptor es el mismo emisor y objeto_imp es 01. El XML se valida además contra el XSD oficial (cfdv40 + ComercioExterior20).
  • Catálogos para el formulario: /catalogos/pais, cce-incoterm, cce-clave-pedimento, cce-unidad-aduana, cce-motivo-traslado, cce-fraccion-arancelaria?q=.

Lo que sigue validando solo el PAC (Lite no puede comprobarlo sin una fuente oficial confiable): que tipo_cambio_usd sea el oficial del DOF, la existencia real del Tax ID, la vigencia de la fracción arancelaria y que unidad_aduana sea su unidad de tarifa, y la fracción de muestras 9801000100. En sandbox esas reglas no se evalúan. No incluye el complemento de Leyendas Fiscales.

Timbrado Masivo (Batch)

Envía hasta 500 comprobantes en un solo request. Procesamiento asíncrono con polling.

Requiere plan Pro o superior. POST /timbrado/batch responde 403 en Free/Starter. Consultar el estado de lotes existentes (GET /timbrado/batch/{job_id}) no está restringido.

Enviar lotebash
curl -X POST https://lite.senhub.mx/api/v1/timbrado/batch \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lote-junio-001" \
  -d '{
    "items": [
      {"kind": "factura", "client_ref": "fila-1", "data": { /* mismo body que POST /timbrado/timbrar */ }},
      {"kind": "nomina", "client_ref": "fila-2", "data": { /* mismo body que POST /timbrado/nomina */ }},
      {"kind": "pago", "client_ref": "fila-3", "data": { /* mismo body que POST /timbrado/pago */ }},
      {"kind": "retencion", "client_ref": "fila-4", "data": { /* mismo body que POST /timbrado/retencion */ }}
    ]
  }'
# → 202 Accepted
# Polling: GET /timbrado/batch/{job_id}

Nota: Máximo 500 items por lote (413 BATCH_TOO_LARGE si lo excedes). Cada item consume 1 crédito solo si el PAC timbra; si falla, no se cobra.

POST
/timbrado/batch/preflight

Valida esquema y saldo sin timbrar ni cobrar (Pro+)

POST
/timbrado/batch

Crear lote (202; Pro+)

GET
/timbrado/batch

Listar tus lotes

GET
/timbrado/batch/{job_id}?include_items=false

Estado y contadores (polling ligero)

GET
/timbrado/batch/{job_id}/items?limit=200&offset=0

Resultado por ítem (límite máx. 1000)

POST
/timbrado/batch/{job_id}/resume

Reanudar ítems pendientes (idempotente)

GET
/timbrado/batch/{job_id}/export

ZIP con XML + resultados.xlsx (+ errores.xlsx)

Saldo antes de enviar

preflight devuelve folios necesarios, disponibles y faltantes (sufficient). Si el saldo no alcanza al crear el lote, responde 402 INSUFFICIENT_CREDITS con missing_credits y no crea nada. Los ítems sandbox no consumen folios.

Si el saldo se agota a media corrida, el lote queda paused_no_credits (los pendientes siguen pending, no son error): recarga y usa resume.

Estados del lote

  • pending · processing (en curso)
  • completed · completed_with_errors · failed (terminales)
  • paused_no_credits (espera: avisa y ofrece recargar)

Códigos de error por ítem

  • BUILD_OR_SIGN: no se pudo construir o sellar (datos o CSD inválidos). Corrige y envía un lote nuevo.
  • PAC_ERROR: el PAC rechazó el timbrado; no se cobra el folio.
  • PREVIOUS_ATTEMPT_FAILED: un intento previo ya había fallado.
  • INTERNAL: error inesperado; cita el id del lote al contactar soporte.

Los ítems con error nunca se reintentan solos (podría duplicarse un CFDI real). client_ref es opcional, único por lote y vuelve tal cual en cada ítem: úsalo para cruzar tu fila con su UUID sin depender del orden.

Seguimiento: consulta cada 5–10 s y respeta Retry-After ante un 429: el límite es de 30 peticiones por minuto por IP en /timbrado/*, e incluye el polling. Mejor aún, suscribe el webhook batch.completed. Cerrar la pestaña no detiene el lote: corre en el servidor.

Nómina 1.2

Complemento de Nómina para recibos de sueldo, aguinaldo, liquidaciones y más.

POST /timbrado/nominajson
{
  "emisor_rfc": "EKU9003173C9",
  "emisor_nombre": "MI EMPRESA SA DE CV",
  "emisor_regimen": "601",
  "lugar_expedicion": "06600",
  "serie": "NOM",
  "folio": "001",
  "receptor": {
    "rfc": "CACX7605101P8",
    "nombre": "CARLOS CASTRO XALAPA",
    "domicilio_fiscal_receptor": "62000",
    "regimen_fiscal_receptor": "605",
    "curp": "CACX760510HMSRRL09",
    "tipo_contrato": "01",
    "tipo_regimen": "02",
    "num_empleado": "EMP-001",
    "periodicidad_pago": "04",
    "clave_ent_fed": "MOR",
    "num_seguridad_social": "12345678901",
    "fecha_inicio_rel_laboral": "2020-01-15",
    "antiguedad": "P280W",
    "salario_diario_integrado": 850.00
  },
  "nomina": {
    "tipo_nomina": "O",
    "fecha_pago": "2026-06-15",
    "fecha_inicial_pago": "2026-06-01",
    "fecha_final_pago": "2026-06-15",
    "num_dias_pagados": 15,
    "registro_patronal": "A1234567890",
    "percepciones": [
      {"tipo_percepcion": "001", "clave": "P001", "concepto": "Sueldo quincenal", "importe_gravado": 12000.00, "importe_exento": 0.00},
      {"tipo_percepcion": "005", "clave": "P005", "concepto": "Prima vacacional", "importe_gravado": 800.00, "importe_exento": 200.00}
    ],
    "deducciones": [
      {"tipo_deduccion": "002", "clave": "D002", "concepto": "ISR", "importe": 2100.00},
      {"tipo_deduccion": "001", "clave": "D001", "concepto": "Seguridad Social", "importe": 450.00}
    ],
    "otros_pagos": [
      {"tipo_otro_pago": "002", "clave": "OP002", "concepto": "Subsidio para el empleo", "importe": 407.02, "subsidio_al_empleo": 407.02}
    ]
  }
}

Complemento de Pagos 2.0 (REP)

Recibos Electrónicos de Pago para parcialidades o pagos diferidos.

POST /timbrado/pagojson
{
  "emisor_rfc": "EKU9003173C9",
  "emisor_nombre": "MI EMPRESA SA DE CV",
  "emisor_regimen": "601",
  "lugar_expedicion": "06600",
  "serie": "PAG",
  "folio": "001",
  "receptor": {
    "rfc": "XAXX010101000",
    "nombre": "PUBLICO EN GENERAL",
    "domicilio_fiscal": "06600",
    "regimen_fiscal": "616"
  },
  "pagos": [{
    "fecha_pago": "2026-06-20T10:30:00",
    "forma_de_pago_p": "03",
    "moneda_p": "MXN",
    "num_operacion": "REF-SPEI-123456",
    "docto_relacionado": [{
      "id_documento": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
      "moneda_dr": "MXN",
      "num_parcialidad": 1,
      "imp_saldo_ant": 11600.00,
      "imp_pagado": 11600.00,
      "objeto_imp_dr": "02",
      "impuestos_dr": {
        "traslados_dr": [{
          "base_dr": 10000.00,
          "impuesto_dr": "002",
          "tipo_factor_dr": "Tasa",
          "tasa_o_cuota_dr": "0.160000",
          "importe_dr": 1600.00
        }]
      }
    }]
  }]
}

Validaciones importantes

  • • forma_de_pago_p ≠ "99" (prohibido "Por definir" en REP)
  • • moneda_p ≠ "XXX" (solo el comprobante padre usa XXX)
  • • tipo_cambio_p requerido si moneda_p ≠ "MXN"
  • • imp_pagado ≤ imp_saldo_ant
  • • id_documento debe ser UUID válido del CFDI original

Retenciones 2.0

Constancias de retención de ISR, IVA, dividendos y más.

POST /timbrado/retencionjson
{
  "emisor_rfc": "EKU9003173C9",
  "emisor_nombre": "MI EMPRESA SA DE CV",
  "emisor_regimen": "601",
  "lugar_expedicion": "06600",
  "cve_retenc": "01",
  "folio_int": "RET-2026-001",
  "receptor": {
    "nacionalidad_r": "Nacional",
    "nacional": {
      "rfc_r": "CACX7605101P8",
      "nom_den_raz_soc_r": "CARLOS CASTRO XALAPA",
      "domicilio_fiscal_r": "62000",
      "curp_r": "CACX760510HMSRRL09"
    }
  },
  "periodo": {
    "mes_ini": "01",
    "mes_fin": "06",
    "ejercicio": "2026"
  },
  "totales": {
    "monto_tot_operacion": 100000.00,
    "monto_tot_grav": 100000.00,
    "monto_tot_exent": 0.00,
    "monto_tot_ret": 10000.00,
    "imp_retenidos": [{
      "base_ret": 100000.00,
      "impuesto_ret": "01",
      "monto_ret": 10000.00,
      "tipo_pago_ret": "03"
    }]
  }
}

Receptor extranjero

Cuando el beneficiario reside en el extranjero, usa nacionalidad_r: "Extranjero" y el nodo extranjero en lugar de nacional. Para pagos a residentes en el extranjero, el catálogo cve_retenc incluye las claves 17 y 18 (a tasa del 25 %); búscalas en los catálogos.

receptor — extranjerojson
"receptor": {
  "nacionalidad_r": "Extranjero",
  "extranjero": {
    "nom_den_raz_soc_r": "ACME INC",
    "num_reg_id_trib_r": "123456789"
  }
}

num_reg_id_trib_r (número de registro de identidad tributaria del país de residencia) es opcional en el contrato de la API.

Cancelación de CFDI

Cancela un CFDI timbrado. Sin costo: cobramos por emitir, no por el ciclo de vida de un comprobante ya pagado. El motivo de cancelación es el catálogo oficial del SAT.

POST /timbrado/cancelarbash
curl -X POST https://lite.senhub.mx/api/v1/timbrado/cancelar \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "emisor_rfc": "EKU9003173C9",
    "uuid": "B765E38F-1234-5678-ABCD-EF1234567890",
    "motivo": "02",
    "folio_sustitucion": null
  }'

Cómo leer la respuesta

Lite no da por cancelado un CFDI solo porque el PAC aceptó la solicitud: confirma el estatus real contra el SAT.

  • status: canceled: el SAT confirmó. Se emite el webhook timbre.cancelled.
  • status: stamped + cancel_status: pending: solicitud aceptada, el CFDI sigue vigente hasta que el SAT confirme (p. ej. el receptor debe aceptar). Lite consulta cada 5 min; repetir POST /cancelar solo consulta.
  • cancel_status: rejected (el receptor rechazó) o not_applied (el SAT no la reflejó en 2 h): puedes volver a solicitarla.

Lista las solicitudes en curso con GET /timbrado/timbres/buscar?cancel_status=pending. La cancelación no usa Idempotency-Key (cancelar algo ya cancelado devuelve 200 sin cobro). Solo se cancelan CFDI timbrados por Lite; otro UUID responde 400 NOT_FOUND. Una sk_test no puede cancelar CFDI reales (403). Para sustituir un CFDI: emite el nuevo con cfdi_relacionados tipo 04 y cancela el original con motivo 01 y folio_sustitucion.

CampoDescripción
emisor_rfcRFC del emisor que timbró el comprobante.
uuidUUID fiscal del CFDI a cancelar.
motivo"01" Comprobante con errores con relación · "02" Errores sin relación · "03" No se llevó a cabo la operación · "04" Operación nominativa en factura global.
folio_sustitucionUUID del CFDI sustituto. Obligatorio cuando el motivo es "01"; en los demás motivos envía null u omítelo.

Consulta y Descarga

Accede al historial de timbres y descarga XML/PDF en cualquier momento.

GET
/timbrado/timbres

Listar timbres (simple, solo limit)

GET
/timbrado/timbres/buscar

Historial con filtros y paginación (offset o cursor)

GET
/timbrado/cfdi/{uuid}

Resumen de un CFDI

GET
/timbrado/cfdi/{uuid}/detalle

CFDI parseado del XML: conceptos e impuestos

GET
/timbrado/cfdi/{uuid}/saldo

Saldo de una factura PPD (total − pagos vigentes)

GET
/timbrado/cfdi/{uuid}/xml

Descargar XML timbrado

GET
/timbrado/cfdi/{uuid}/pdf

Descargar PDF generado (diseño SenHub, gratuito y cacheado)

GET
/timbrado/cfdi/{uuid}/html

Vista HTML del CFDI

Historial con filtros: /timbrado/timbres/buscar

Devuelve un sobre paginado { items, total, limit, offset, has_more, next_cursor }. Todos los parámetros son opcionales.

  • status (draft|stamped|canceled|error), tipo_comprobante (I, E, T, N, P), rfc_emisor, rfc_receptor, serie, environment
  • date_from/date_to con date_field=created|stamped; q busca parcial en UUID o folio
  • sort=fecha|total|rfc_emisor, order=desc|asc, limit (1–200)
  • idempotency_key concilia un intento contra tu pedido; cancel_status lista cancelaciones en curso

Paginación: por offset (suma limit mientras has_more) o por cursor (reenvía next_cursor; recomendado a gran escala; solo con el orden por defecto, si no responde 422). has_xml/has_pdf indican disponibilidad; descarga con los endpoints de arriba. El PDF por defecto también admite ?inline=true para verlo en el navegador.

PDF con marca

Plan Pro+

Genera el PDF con plantilla, color de acento y logo propios de tu cliente. Es un endpoint aparte para no alterar el GET /pdf por defecto, que sigue siendo el diseño SenHub, gratuito y cacheado. Los cinco campos del body son opcionales: sin ellos no pasa nada distinto a hoy, así que integrarlo no rompe nada de tu consumo actual.

POST
/timbrado/cfdi/{uuid}/pdf

PDF con marca (attachment)

POST
/timbrado/cfdi/{uuid}/pdf?inline=true

PDF con marca (visor en navegador)

GET
/timbrado/plantillas-pdf

Listar plantillas disponibles (requiere JWT Pro+)

¿Quién puede usarlo?

Free · Starter403Ni siquiera se llama al renderer. Cero costo.
Pro · Enterprise200Personalización activa. El branding es feature de plan Pro o superior.

Solo aplica a facturas (tipo I o E). Para nómina, pagos/REP, traslados o retenciones usa el GET /pdf (diseño SenHub); el POST responde 422 si el UUID no es una factura.

Si el servicio de render no está disponible, el endpoint hace fallback automático al PDF SenHub local: recibes un PDF válido, nunca un error. Ante cualquier 4xx/5xx, guarda la cabecera X-Request-Id de la respuesta: es la referencia para rastrear la operación.

Body — todos los campos son opcionales

CampoTipoDescripción
template_idstringID de plantilla (ver galería abajo o GET /timbrado/plantillas-pdf). Si se omite, se usa la plantilla default.
accent_colorstringColor de acento hexadecimal (#RGB o #RRGGBB).
logo_base64stringLogo PNG/JPG en Base64 (con o sin prefijo data:). Máx. 512 KB.
notesstringNotas comerciales bajo el desglose de totales. Respeta saltos de línea \n.
page_formatstringA4 (default) o Letter.

Reglas de cascada

  • Los tres campos de marca son opcionales e independientes.
  • Sin template_id → plantilla default, se omiten logo_base64 y accent_color (PDF limpio, sin marca).
  • Con template_id pero sin logo_base64 → la plantilla se renderiza sin logo.
  • Con template_id pero sin accent_color → se usa el color propio de esa plantilla.
  • No se cachea: cada POST refleja exactamente la marca enviada, sin pisar el PDF SenHub del GET /pdf.

Seguridad y garantías

  • El XML fiscal no cambia: la marca es solo representación impresa.
  • No se acepta ninguna URL de logo remota (solo logo_base64).
  • Color, formato o logo inválidos → 400 con mensaje claro.
Generar PDF con marcabash
curl -X POST https://lite.senhub.mx/api/v1/timbrado/cfdi/$UUID/pdf \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "stripe-style@1",
    "accent_color": "#059669",
    "logo_base64": "'"$(base64 -w0 logo.png)"'"
  }' \
  -o factura.pdf

No hardcodees esta configuración. Tu front puede leer el bloque pdf_con_marca del contrato de integración (disponible, plan_minimo, comprobantes_soportados, campos y reglas) para saber si mostrar la personalización y con qué reglas, sin fijar nada en código.

Plantillas disponibles

Vista previa con datos de muestra. Usa el template_id de cada plantilla en el body del POST …/pdf. La lista viva siempre está en GET /timbrado/plantillas-pdf.

Modern Clean

Minimalista

Minimalista elegante, tipografía premium. Es la plantilla por defecto.

Stripe Style

Moderno

Limpio y moderno, inspirado en la facturación de Stripe.

Glass Purple

Moderno

Glassmorphism moderno con acentos violeta, estilo SaaS.

Corporate Dark

Corporativo

Header oscuro ejecutivo, elegante para empresas.

Blue Enterprise

Corporativo

Corporativo azul formal, estilo SAP / ERP.

SAT Classic Pro

Clásico

SAT tradicional modernizado, ideal para contadores.

Modern Table

Moderno

Enfocado en la tabla; ideal para facturas con muchos conceptos.

Startup Gradient

Moderno

Header con degradado, estilo startup de Silicon Valley.

Minimal Receipt

Minimalista

Ultra-compacto tipo ticket, pensado para POS.

Luxury Gold

Premium

Premium con acentos dorados para marcas de lujo.

SenHub Signature

Premium

Insignia SenHub: violeta futurista con acabado enterprise.

Clientes · receptores reutilizablesStarter+

CRUD de receptores (clientes) con datos fiscales prevalidados, expuesto en la API como el recurso /customers. Guarda una vez a tus clientes frecuentes y timbra con customer_id en lugar de repetir el receptor completo en cada factura.

Disponible desde el plan Starter. En Free, estos endpoints responden 403. Es una capacidad 100% opcional: puedes seguir enviando el receptor completo en el payload de timbrado como siempre.

POST
/customers

Crear customer (RFC único por cuenta)

GET
/customers

Listar / buscar (?q=, ?active_only=, ?limit=, ?offset=)

GET
/customers/{id}

Obtener un customer

PUT
/customers/{id}

Actualizar (campos parciales)

DELETE
/customers/{id}

Eliminar (soft-delete, is_active=false)

Crear customerbash
curl -X POST https://lite.senhub.mx/api/v1/customers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rfc": "XAXX010101000",
    "nombre": "Público en General",
    "domicilio_fiscal": "44100",
    "regimen_fiscal": "616",
    "uso_cfdi": "S01",
    "email": "contacto@cliente.mx",
    "telefono": "3312345678",
    "notas": "Cliente frecuente"
  }'
  • El RFC se normaliza a mayúsculas y es único por cuenta (RFC duplicado → 409 Conflict).
  • Una vez creado, timbra con customer_id (ver Timbrado simplificado).
  • El borrado es lógico (DELETE → is_active=false), así los CFDI históricos conservan su referencia.

Sync offline (idempotente)

El servidor es la fuente de verdad; tu app puede funcionar como caché con cola offline. Aplica igual para Productos.

GET
/customers/sync

Pull incremental (?since=ISO&limit=500); incluye inactivos para reflejar bajas

POST
/customers/bulk

Upsert por lote (máx 500) por client_ref

  • Al crear puedes enviar tu client_ref (id local): si ya existe, la API devuelve el registro existente en vez de duplicar. Reintentar es seguro.
  • Guarda el server_time de cada respuesta y úsalo como el próximo since. Repite mientras has_more sea true.

Productos · conceptos reutilizablesStarter+

CRUD de productos/servicios con sus datos fiscales y cálculo de impuestos, expuesto en la API como el recurso /products. Guarda tus conceptos frecuentes y timbra con product_id en lugar de repetir el desglose de impuestos en cada línea.

Disponible desde el plan Starter. En Free responden 403. Igual que Customers, es 100% opcional: los conceptos inline completos siguen funcionando sin cambios.

POST
/products

Crear product

GET
/products

Listar / buscar (?q= por descripción o clave SAT)

GET
/products/{id}

Obtener un product

PUT
/products/{id}

Actualizar (campos parciales)

DELETE
/products/{id}

Eliminar (soft-delete)

GET
/products/sync

Pull incremental (?since=ISO&limit=500)

POST
/products/bulk

Upsert por lote (máx 500) por client_ref

Crear productbash
curl -X POST https://lite.senhub.mx/api/v1/products \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "descripcion": "Servicio de consultoría TI",
    "clave_prod_serv": "81111500",
    "clave_unidad": "E48",
    "valor_unitario": 1160.00,
    "tasa_iva": 0.16,
    "precio_incluye_iva": true,
    "no_identificacion": "SKU-CONSUL-001",
    "notas": "Servicio mensual"
  }'

Campos de impuestos por defecto

CampoDescripción
tasa_iva0.16 = IVA 16 %, 0 = IVA 0 %, null = exento
tasa_iepsTraslado IEPS (003) como Tasa, ej. 0.08. null = no aplica
ret_ivaRetención de IVA (002), ej. 0.106667. null = no aplica
ret_isrRetención de ISR (001), ej. 0.10. null = no aplica
precio_incluye_ivaSi true, el valor_unitario incluye IVA y se desglosa solo al timbrar
objeto_imp"02" = sí objeto de impuesto (default), "01" = no
no_identificacionSKU propio (opcional)
client_refReferencia externa para sync offline idempotente (única por cuenta)

Los impuestos por defecto se aplican como traslados/retenciones del concepto al timbrar con product_id. Para casos que requieren control manual (tipo_factor: "Cuota", importes fijos), puedes sobrescribirlos en el request de timbrado — los campos inline siempre prevalecen sobre el catálogo.

Créditos

Cada operación de timbrado consume 1 crédito. Consulta tu saldo programáticamente para implementar alertas de bajo saldo en tu sistema.

GET
/creditos/balance

Saldo actual y plan activo

GET
/creditos/paquetes

Paquetes disponibles con su precio

GET
/creditos/movimientos

Ledger de movimientos (compras, timbres, reembolsos)

GET
/pagos/cotizar?quantity=435

Cotiza una cantidad exacta: subtotal, IVA y total (sin cobro)

POST
/pagos/checkout

Inicia el cobro (Stripe PaymentIntent)

GET
/pagos/{payment_intent_id}/status

Estado del pago y de la acreditación

GET
/pagos/historial?limit=50

Historial de pagos

Consultar saldojson
GET /api/v1/creditos/balance
Authorization: Bearer $TOKEN

{
  "balance": 847,
  "plan": "pro"
}

Gestión de créditos

La compra de créditos, historial de pagos y configuración de auto-recarga se gestionan desde tu Developer Dashboard.

  • Compra paquetes o cantidades custom
  • Los créditos no expiran
  • Si un timbre falla, el crédito se reembolsa automáticamente
  • Si tu saldo llega a 0, la API responde 402 Saldo insuficiente

Comprar créditos por API (Stripe)

El importe lo calcula siempre el servidor: nunca envíes un monto, solo package_code o quantity (exactamente uno). Todos los precios son netos; al cobrar se suma IVA 16 %. Las tarifas vigentes están en Planes y Precios.

  1. POST /pagos/checkout con { "package_code": "PACK_100" } o { "quantity": 435 } → client_secret, publishable_key, payment_intent_id, amount (centavos).
  2. Confirma la tarjeta en tu front con Stripe Elements usando el client_secret.
  3. Stripe avisa al backend por webhook firmado, que acredita los créditos (idempotente).
  4. Consulta GET /pagos/{payment_intent_id}/status hasta credits_assigned: true.

El saldo se confirma con credits_assigned: true, no con el resultado de confirmPayment del navegador. Antes de timbrar, esa es la señal para continuar. POST /creditos/comprar está deshabilitado en producción (acreditaba sin cobro).

Recomendación para tu backend

// Verificar saldo antes de timbrar (opcional pero recomendado)
const { balance } = await fetch("/api/v1/creditos/balance", { headers }).then(r => r.json());

if (balance < 10) {
  // Alerta interna: "Saldo bajo, recargar desde dashboard"
  notifyAdmin("SenHub credits low: " + balance);
}

Webhooks (Plan Pro+)

Registra URLs para recibir notificaciones cuando ocurren eventos en tu cuenta. Cada entrega se firma con HMAC-SHA256 para verificar su autenticidad. Disponible en planes Pro y Enterprise.

EventoCuándo se dispara
timbre.createdSe emitió un CFDI exitosamente.
timbre.cancelledEl SAT confirmó la cancelación (no al aceptar la solicitud).
batch.completedUn lote terminó, con o sin errores. No se emite al pausar por saldo; sí cuando el lote reanudado termina. data: job_id, status, total_items, success_count, error_count.

Usa "events": ["*"] para suscribirte a todos.

POST
/webhook-endpoints

Registrar endpoint ({ url, events, description })

GET
/webhook-endpoints

Listar

GET
/webhook-endpoints/{id}?reveal_secret=true

Obtener (opcionalmente con el secreto)

PUT
/webhook-endpoints/{id}

Actualizar

DELETE
/webhook-endpoints/{id}

Eliminar (soft-delete)

POST
/webhook-endpoints/{id}/rotate-secret

Rotar el signing secret

Registrar un endpointbash
curl -X POST https://lite.senhub.mx/api/v1/webhook-endpoints   -H "Authorization: Bearer $TOKEN"   -H "Content-Type: application/json"   -d '{
    "url": "https://mi-servidor.com/webhooks/senhub",
    "events": ["timbre.created", "batch.completed"],
    "description": "Servidor de producción"
  }'

# → 201 { "id": "...", "signing_secret": "whsec_abc123...", "is_active": true, ... }
# Guarda el signing_secret para verificar firmas.

Reglas de la URL

  • Debe ser https y apuntar a un destino público. Se rechaza (422) localhost, nombres internos, IP privadas o reservadas, notaciones no estándar (127.1), URLs con usuario y contraseña, y dominios que no resuelven.
  • Se revalida antes de cada envío y no se siguen redirecciones.
  • Máximo 5 endpoints activos por cuenta.

Entrega y reintentos

  • Se entrega en segundo plano: un endpoint lento no retrasa tu timbrado.
  • 3 intentos (espera de 1 s, 5 s y 15 s; 10 s de timeout). Se espera 2xx; no hay reenvío persistente después.
  • Responde rápido y procesa en segundo plano; si pierdes un evento, consulta el estado con la API.
Payload y cabeceras recibidasjson
// Cabeceras: X-SenHub-Signature: t=1718816400,v1=abc123...
//            X-SenHub-Event: timbre.created
//            Content-Type: application/json   User-Agent: SenHub-Webhook/1.0
{
  "event": "timbre.created",
  "created_at": "2026-06-19T17:00:00+00:00",
  "resource_id": "uuid-del-cfdi",
  "data": { "uuid": "ABC-123-DEF", "rfc_emisor": "EKU9003173C9", "tipo": "I", "total": 11600.00 }
}
Verificar la firma (Python)python
import hmac, hashlib

def verify_signature(payload_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = f"{parts['t']}.{payload_body.decode()}"
    computed = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, parts["v1"])
Verificar la firma (Node.js)javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Firma el cuerpo crudo tal como llega, no el JSON re-serializado.

Recibos y Autofactura

Crea un ticket digital con una URL pública: tu cliente final ingresa sus datos fiscales y se genera un CFDI real. Un recibo no es un CFDI; el CFDI se emite al facturarlo.

POST
/receipts

Crear recibo (items hasta 5000, forma_pago, customer_id, external_id, branch, expires_hours)

GET
/receipts

Listar con filtros: status, customer_id, payment_form, q, date_gte, date_lte, limit, offset

GET
/receipts/{id}

Detalle

PUT
/receipts/{id}

Actualizar (asignar cliente, notas)

POST
/receipts/{id}/cancel

Cancelar (solo si está open)

GET
/receipts/{id}/pdf

PDF del recibo (no es un CFDI)

POST
/receipts/global-invoice

Factura global de público en general con recibos abiertos

POST
/receipts/to-invoice

Facturar varios recibos a un cliente (dry_run disponible)

Crear un recibobash
curl -X POST https://lite.senhub.mx/api/v1/receipts   -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"   -d '{
    "emisor_rfc": "EKU9003173C9", "emisor_nombre": "ESCUELA KEMPER URGATE",
    "emisor_regimen": "601", "lugar_expedicion": "44100",
    "items": [{ "descripcion": "Consultoría IT", "cantidad": 2, "valor_unitario": 500.00,
                "clave_prod_serv": "81111500", "clave_unidad": "E48", "tasa_iva": 0.16 }],
    "folio_ticket": "T-001", "expires_hours": 72
  }'
# → 201 { "id": "...", "key": "...", "folio_number": 1, "total": 1160.0, "status": "open",
#         "self_invoice_url": "https://.../autofactura/<key>", "expires_at": "..." }

Autofactura pública (sin JWT)

  • GET /self-invoice/{key}: datos del recibo y can_invoice.
  • POST /self-invoice/{key} con rfc, nombre, domicilio_fiscal, regimen_fiscal, uso_cfdi → genera el CFDI.
  • GET /self-invoice/{key}/status: estado del CFDI.
  • Micrositio HTML: GET /autofactura/{key} (formulario listo, estados abierto, facturado, cancelado, expirado).

Reglas

  • Estados: open, invoiced, expired, cancelled, invoiced_globally.
  • El token expira (por defecto 72 h; configurable de 1 a 720). 1 CFDI por recibo: no se re-factura.
  • Se cobra 1 crédito al generar el CFDI; el emisor necesita un CSD activo.
  • periodicity de la factura global: day, week, fortnight, month, two_months; los recibos incluidos pasan a invoiced_globally.
  • to-invoice: de 1 a 1000 keys; sin customer_id, todos deben tener el mismo cliente asignado. Con dry_run: true valida sin crear el CFDI.

Series, Email y Herramientas

POST
/series

Crear serie ({ name, description, start_folio, is_default })

GET
/series

Listar series

PUT
/series/{id}

Actualizar descripción o default

POST
/series/{id}/default

Establecer como predeterminada

DELETE
/series/{id}

Eliminar

POST
/email/cfdi/{uuid}

Enviar un CFDI timbrado por correo (XML y/o PDF)

POST
/email/receipt/{receipt_id}

Enviar un recibo por correo

POST
/tools/validate-rfc

Valida la estructura de un RFC

POST
/tools/verify-email

Verifica la capacidad de entrega de un correo

Series y folios

El nombre se normaliza a MAYÚSCULAS y es único por cuenta. Solo una serie puede ser predeterminada; al elegir otra, la anterior se desactiva. El folio se asigna de forma atómica, sin duplicados bajo concurrencia.

Envío por email

Body: to (hasta 50), subject, message, attach_xml y attach_pdf (por defecto true). Responde 404 si el CFDI no existe y 502 si el proveedor de correo falla.

Herramientas

validate-rfc detecta moral, fisica, generico_nacional y generico_extranjero (solo estructura, no existencia ante el SAT). verify-email es fail-open: si el proveedor falla, devuelve deliverable: true.

Catálogos SAT

70 catálogos SAT actualizados: 64 chicos que puedes traer completos y 6 pesados que solo se consultan por búsqueda. Úsalos para autocompletados y validaciones en tu frontend.

GET
/catalogos

Índice vivo: qué catálogos existen y cuáles son pesados

GET
/catalogos/{nombre}

Consultar un catálogo (acepta q, limit y meta)

Descubre los catálogos en runtime, no los hardcodees

GET /catalogos es la fuente de verdad: publica el nombre de cada catálogo y si es pesado. Si lees ese índice al arrancar tu app (y lo cacheas unas horas), tu integración no se desincroniza cuando el SAT o nosotros agregamos catálogos. Una lista fija en tu código sí se queda atrás.

Parámetros comunes

Los tres aplican a todos los catálogos, chicos y pesados.

ParámetroComportamiento
qFiltra por código (exacto o prefijo) o por texto: ignora acentos y acepta varias palabras. Opcional en los chicos, obligatorio en los pesados.
limitChicos: default 500, máximo 1000. Pesados: default 10–15, máximo 50.
meta=trueDevuelve un sobre con metadatos en lugar del array plano. Útil para saber si el resultado se truncó por limit.

Catálogos chicos

Sin q devuelven la lista completa como [{ code, label, extra? }]. Traélos una vez y cáchealos en tu app.

forma-pago · 28metodo-pago · 2moneda · 183regimen-fiscal · 23uso-cfdi · 25tipo-comprobante · 5tipo-relacion · 9impuesto · 3estado · 96+55 más en el índice
Catálogo completo y filtradobash
# Lista completa
curl https://lite.senhub.mx/api/v1/catalogos/regimen-fiscal \
  -H "Authorization: Bearer $TOKEN"
# → [{ "code": "601", "label": "General de Ley Personas Morales" }, ...]

# Filtrar por texto (sin acentos, multi-palabra)
curl "https://lite.senhub.mx/api/v1/catalogos/moneda?q=peso" \
  -H "Authorization: Bearer $TOKEN"          # → 11 resultados

# Filtrar por código exacto
curl "https://lite.senhub.mx/api/v1/catalogos/moneda?q=MXN" \
  -H "Authorization: Bearer $TOKEN"          # → 1 resultado

# Acotar y buscar ignorando acentos
curl "https://lite.senhub.mx/api/v1/catalogos/pais?limit=5" \
  -H "Authorization: Bearer $TOKEN"          # → 5 resultados
curl "https://lite.senhub.mx/api/v1/catalogos/pais?q=mexico" \
  -H "Authorization: Bearer $TOKEN"          # → "México"

Catálogos pesados: la búsqueda es obligatoria

clave-prod-serv (más de 52,000 claves) y clave-unidad no se descargan completos: exigen q. Perfectos para un autocompletado con debounce de ~300 ms.

Buscar ClaveProdServ y ClaveUnidadbash
# Prefijo numérico de la clave (productos)
curl "https://lite.senhub.mx/api/v1/catalogos/clave-prod-serv?q=4321&limit=15" \
  -H "Authorization: Bearer $TOKEN"
# → [{ "code": "43211500", "descripcion": "..." }, ...]

# Prefijo numérico de la clave (servicios)
curl "https://lite.senhub.mx/api/v1/catalogos/clave-prod-serv?q=8510&limit=15" \
  -H "Authorization: Bearer $TOKEN"
# → [{ "code": "85101600", "descripcion": "..." }, ...]

# Texto libre (unidades)
curl "https://lite.senhub.mx/api/v1/catalogos/clave-unidad?q=pieza&limit=15" \
  -H "Authorization: Bearer $TOKEN"
# → [{ "code": "H87", "nombre": "Pieza" }, ...]

Ojo con el nombre del campo descriptivo: los chicos responden label, clave-prod-serv responde descripcion y clave-unidad responde nombre. El campo code es siempre el que envías al timbrar. Si reutilizas un mismo componente de autocompletado, resuelve la etiqueta con label ?? descripcion ?? nombre y evitarás opciones en blanco.

Códigos de Error

Respuestas JSON consistentes con detalle accionable. Esta tabla se genera desde el contrato de integración que sirve la propia API, versión 2026-10-20.5.

Toda respuesta incluye la cabecera X-Request-Id. citar este id al reportar un problema a soporte. Muéstrala en tus propios mensajes de error: es la referencia con la que podemos rastrear una operación concreta.

HTTPcode¿Se timbró?Folios¿Reintentar?
400PAC_ERRORNo0Sí, con la misma llave
400BUILD_OR_SIGNNo0Sí, con la misma llave
402INSUFFICIENT_CREDITSNo0No
401UNAUTHORIZEDNo0No
403SUBSCRIPTION_REQUIREDNo0No
422VALIDATION_ERRORNo0Sí, con la misma llave
429RATE_LIMITEDNo0Sí, con la misma llave
500INTERNAL_ERRORIndeterminadoIndeterminadoSí, con la misma llave
400PAC_ERROREl PAC rechazó el comprobante

El comprobante no se timbró. El PAC devolvió una incidencia, por ejemplo CFDI40144 cuando el nombre fiscal no coincide con la constancia del SAT.

Qué hacer: Corregir los datos y reintentar. El cargo de folios se reembolsa automáticamente.

400BUILD_OR_SIGNError al construir o sellar el CFDI

El payload no pudo convertirse en un CFDI válido, o el sellado con el CSD falló. No se llegó al PAC.

Qué hacer: Revisar catálogos, importes y vigencia del CSD.

402INSUFFICIENT_CREDITSSin folios disponibles

No hay saldo para timbrar. Nunca ocurre en sandbox.

Qué hacer: Comprar folios y reintentar.

401UNAUTHORIZEDCredencial ausente o inválida

El token o la API Key no son válidos.

Qué hacer: Reautenticar.

403SUBSCRIPTION_REQUIREDSuscripción inactiva

La API Key requiere una suscripción activa para uso servidor a servidor.

Qué hacer: Activar o renovar la suscripción.

422VALIDATION_ERRORPayload inválido

Falta un campo obligatorio o un tipo es incorrecto. Lo valida el esquema antes de construir el CFDI.

Qué hacer: Corregir el payload según el detalle por campo.

429RATE_LIMITEDLímite de peticiones excedido

Se superó el límite por minuto.

Qué hacer: Esperar los segundos de la cabecera Retry-After y reintentar con la misma Idempotency-Key.

500INTERNAL_ERRORError interno

Falla inesperada de nuestro lado. IMPORTANTE: el comprobante PUEDE haberse timbrado. No asumas que no existe.

Qué hacer: Reintentar con la MISMA Idempotency-Key. Si el CFDI ya existía recibirás 200 con el comprobante original y sin cobro nuevo. Nunca reintentes con una llave nueva: eso sí duplicaría el comprobante. Reporta el request_id a soporte.

Idempotencia: la regla de oro

Una factura, una llave. Ante cualquier error reintenta con la MISMA llave; nunca generes una nueva para el mismo comprobante.

Cabecera Idempotency-Key · UUID v4, uno por comprobante lógico · alcance por tenant.

Situación del primer intentoRespuesta al reintentar con la misma llave
Ya se timbró200 · El comprobante original, con el mismo uuid, sin cobrar de nuevo
Había fallado200 · Se intenta timbrar de nuevo

No devuelve 409. Verificado: 2026-07-31, tres intentos con la misma llave devolvieron el mismo uuid.

El 500 es el único caso ambiguo

Ante un 500 el comprobante puede haberse timbrado. No asumas que no existe: reintenta con la misma Idempotency-Key y recibirás el original si ya se emitió. Generar una llave nueva sí duplicaría el comprobante.

500 — Error internojson
{
  "detail": "Internal Server Error",
  "code": "INTERNAL_ERROR",
  "request_id": "9f2c1a7b4e8d4c1fa0b3c5d6e7f80912",
  "retriable": true,
  "retry_with_same_idempotency_key": true
}

No existe un 502 en esta API. Cuando el PAC rechaza o no responde, la respuesta es 400 con code: "PAC_ERROR".

400 — El PAC rechazó el comprobantejson
{
  "detail": {
    "code": "PAC_ERROR",
    "message": "El PAC rechazó el timbrado: CFDI40144 - El nombre del emisor no coincide"
  }
}
422 — Validación del payloadjson
{
  "detail": [
    {"loc": ["body", "receptor", "rfc"], "msg": "String should have at least 12 characters", "type": "string_too_short"},
    {"loc": ["body", "conceptos", 0, "valor_unitario"], "msg": "Input should be greater than 0", "type": "greater_than"}
  ]
}
402 — Sin folios disponiblesjson
{
  "detail": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Saldo de créditos insuficiente"
  }
}

Seguridad y Límites

ParámetroValor
Límite general120 peticiones/min
Límite de timbrado30 peticiones/min
Límite de OTP5 peticiones/min
Conexiones simultáneas50 por IP
Token de acceso (JWT)1 hora
Refresh token30 días (se rota en cada renovación; sin OTP)
CifradoHTTPS obligatorio (TLS 1.2+)
CSD por cuentaSin límite

Sandbox

CFDI válido pero no reportado al SAT. 0 créditos (gratis ilimitado). Ideal para pruebas de integración.

Producción

Timbre fiscal real ante el SAT. 1 crédito por operación exitosa. 5 créditos gratis al registrarte.

Cómo activar el sandbox

Agrega "sandbox": true al payload de cualquier endpoint de timbrado. El servidor decide el ambiente final: una llave sk_test fuerza sandbox siempre, y un CSD de prueba del SAT (p. ej. EKU9003173C9) lo activa automáticamente. Enviar "sandbox": false nunca convierte una prueba en un timbrado real.

Para verificar el ambiente antes de timbrar: GET /timbrado/entorno?emisor_rfc=. El parámetro es opcional, pero envíalo siempre: sin él la respuesta no puede decirte qué ocurrirá con un emisor concreto.

GET /timbrado/entorno?emisor_rfc=EKU9003173C9json
{
  "environment": "production",
  "effective_environment": "sandbox",
  "emisor_rfc": "EKU9003173C9",
  "emisor_is_test_csd": true,
  "sandbox_forced": true,
  "sandbox_forced_reason": "sat_test_csd",
  "sandbox_on_request": true,
  "credits_per_stamp": 0
}
  • environment — ambiente por defecto de la credencial. No describe un comprobante concreto.
  • effective_environment — este es el campo a mostrar y registrar. Ambiente que se aplicará realmente al timbrar con ese emisor_rfc, sin enviar el flag sandbox.
  • emisor_is_test_csd — true si ese RFC es un CSD de prueba del SAT.
  • sandbox_forced — true cuando no es posible timbrar en real con esa combinación de credencial y emisor.
  • sandbox_forced_reason — api_key_test, sat_test_csd o null.
  • sandbox_on_request — el sandbox también puede pedirse por comprobante con "sandbox": true.
  • sandbox_mode — cómo se simula el timbrado, p. ej. local_simulation.
  • credits_per_stamp — folios que costará ese timbrado. 0 confirma sandbox.

Importante: leer solo environment lleva a la conclusión equivocada de que la cuenta está en producción cuando el emisor consultado es un CSD de prueba. Que diga production no significa que el próximo timbrado será real.

Ya en la respuesta del timbrado, toda operación sandbox llega con "sandbox": true, "pac_environment": "sandbox" y "credits_charged": 0. Ojo con la diferencia: credits_per_stamp es la previsión antes de timbrar y credits_charged es lo que se cobró de hecho.

También puedes probarlo sin escribir código: la herramienta web tiene modo pruebas en senhub.mx/herramientas/timbrado?sandbox=1. Activa el checkbox "Modo pruebas" antes de timbrar.

Quickstart

Del registro a tu primer timbre en 5 minutos.

1

Registrarse (5 créditos gratis)

curl -X POST https://lite.senhub.mx/api/v1/auth/otp/request \
  -H "Content-Type: application/json" \
  -d '{"email":"dev@tuapp.com"}'
2

Verificar OTP

curl -X POST https://lite.senhub.mx/api/v1/auth/otp/verify \
  -H "Content-Type: application/json" \
  -d '{"email":"dev@tuapp.com","code":"XXXXXX"}'
# → {"access_token":"eyJ...", "refresh_token":"xxx"}
3

Verificar saldo

curl https://lite.senhub.mx/api/v1/creditos/balance \
  -H "Authorization: Bearer eyJ..."
# → {"balance": 5, "plan": "free"}
4

Subir CSD

curl -X POST https://lite.senhub.mx/api/v1/csd \
  -H "Authorization: Bearer eyJ..." \
  -F "cer=@mi.cer" -F "key=@mi.key" -F "password=xxx"
5

Timbrar tu primer CFDI

curl -X POST https://lite.senhub.mx/api/v1/timbrado/timbrar \
  -H "Authorization: Bearer eyJ..." \
  -H "Content-Type: application/json" \
  -d '{ ... payload CFDI ... }'
# → {"uuid":"B765E38F-...", "xml_url":"...", "pdf_url":"..."}

Preguntas Frecuentes

¿Cuántos RFCs puedo timbrar con una cuenta?

En plan Pro o superior, RFCs ilimitados. En Free/Starter puedes registrar 1 RFC. Renovar un RFC ya registrado siempre está permitido, y el límite nunca afecta el timbrado.

¿Cómo pruebo la integración?

Al crear tu cuenta recibes 5 timbres gratis para validar el flujo completo contra el SAT. Si un timbre falla, el crédito se reembolsa automáticamente.

¿Cómo obtengo API Keys?

Desde el Dashboard, en cualquier plan — incluido el Gratis (no requieren suscripción). Solo cuentan las llaves activas: al revocar una, su cupo queda disponible al instante. Live (sk_live + pk_live): Free 1, Starter 5, Pro 20, Enterprise 100. Prueba (sk_test, cupo propio): Free 3, Starter 5, Pro 10, Enterprise 50. La llave se muestra una sola vez y las sk_ son exclusivamente server-to-server: nunca las uses desde el navegador.

Revoqué una llave y sigue contando en mi cupo, ¿qué hago?

No debería: el cupo solo cuenta llaves activas. Si tu código cuenta los elementos de GET /api-keys, está contando también las revocadas (historial). Usa GET /api-keys/quota o filtra con ?status=active. Para borrar una revocada del historial: DELETE /api-keys/{id}?permanent=true.

¿Qué pasa si un timbre falla?

El crédito se reembolsa automáticamente. Solo pagas por timbres exitosos.

¿Puedo facturar a clientes en el extranjero?

Sí. Usa el RFC genérico XEXX010101000 con régimen 616 y uso S01, el mismo código postal del emisor como domicilio fiscal del receptor y, si cobras en otra divisa, el tipo de cambio real. Hay un ejemplo completo en la sección Timbrado. Para constancias de retención a residentes en el extranjero, usa el receptor Extranjero en Retenciones 2.0. Para exportación de mercancías hay soporte de Comercio Exterior 2.0 (campo comercio_exterior con exportacion 02); consulta la guía de integración o escríbenos.

¿Puedo cancelar CFDIs?

Sí. POST /timbrado/cancelar con el UUID. Cancelar no consume folios: cobramos por emitir, no por el ciclo de vida de un comprobante ya pagado.

¿Soportan complementos?

Sí: Nómina 1.2, Pagos 2.0, Retenciones 2.0, IEDU 1.0. Todos con generación de PDF profesional.

¿El PDF tiene mi marca?

En plan Pro o superior puedes personalizar plantilla, color y logo con POST /timbrado/cfdi/{uuid}/pdf (solo facturas tipo I/E). En Free/Starter el GET /pdf usa el diseño SenHub estándar.

¿Hay postpago?

No. Todos los planes son prepago: compras folios y se descuentan al timbrar. La suscripción cubre el acceso y las capacidades del plan, nunca los folios.

¿Idempotencia?

Sí. Genera un UUID único por factura en el header Idempotency-Key y reutiliza la misma llave al reintentar esa operación. Así evitas doble timbrado por retries o timeouts.

¿Dónde están los endpoints de Clientes y Productos?

En la API v1 son los recursos /customers y /products (con sus variantes /sync y /bulk). Son catálogos reutilizables disponibles desde el plan Starter; en Free responden 403. Una vez que registras un receptor o un concepto, timbras con customer_id / product_id en lugar de repetir el receptor o los conceptos completos. Es 100% opcional: si prefieres, sigues enviando el receptor y los conceptos inline como siempre. Ver las secciones Clientes y Productos arriba.

¿Puedo revender el servicio?

¡Sí! SenHub es infraestructura white-label. Tú pones el precio a tus clientes.

¿Listo para integrar?

Crea tu cuenta, recibe 5 timbres gratis y emite tu primer CFDI en menos de 5 minutos. Sin compromisos.

Soporte por WhatsApp o desde el chat del sitio · contacto@senhub.mx