API REST · CFDI 4.0 · Sandbox gratuito

Timbra CFDI desde tu aplicación

API de facturación electrónica para México. Facturas, nómina, pagos REP y retenciones. Integra en minutos, paga solo por timbre exitoso. Ideal para revendedores.

Comenzar gratis
5 timbres gratis Sandbox ilimitado Integración en 5 min 99.9% uptime

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 30 minutos y un refresh_token de 7 días para renovar 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": "[email protected]"}'

# → {"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": "[email protected]", "code": "123456"}'

# → {
#   "access_token": "eyJhbGciOiJIUzI1NiIs...",
#   "refresh_token": "rt_a1b2c3d4e5f6...",
#   "expires_in": 1800
# }
3. Renovar token (cada ~25 min)bash
curl -X POST https://lite.senhub.mx/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "rt_a1b2c3d4e5f6..."}'

# → {"access_token": "eyJ...(nuevo)...", "expires_in": 1800}
4. Usar en todas las requestsbash
curl https://lite.senhub.mx/api/v1/creditos/balance \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Para tu backend: Guarda el refresh_token en tu base de datos. Programa un cron o middleware que renueve el access_token antes de que expire (cada 25 min). Si el refresh_token expira (7 días sin uso), necesitarás un nuevo OTP.

Certificados (CSD)

Sube los CSD de tus clientes. Sin límite de RFCs por cuenta.

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 "[email protected]" \
  -F "[email protected]" \
  -F "password=MiPassword123"

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)

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.

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 impuestoobjeto_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 impuestosin importe2387.14
Cuota fija

Contrato 2026-08-01.1 · 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"
}

Timbrado Masivo (Batch)

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

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", "emisor_rfc": "...", "emisor_nombre": "...", "emisor_regimen": "601", "payload": {...}},
      {"kind": "nomina", "emisor_rfc": "...", "emisor_nombre": "...", "emisor_regimen": "601", "payload": {...}},
      {"kind": "pago", "emisor_rfc": "...", "emisor_nombre": "...", "emisor_regimen": "601", "payload": {...}}
    ]
  }'
# → 202 Accepted
# Polling: GET /timbrado/batch/{job_id}

Nota: Máximo 500 items por lote. Cada item consume 1 crédito. Si uno falla, su crédito se reembolsa automáticamente.

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_pagadoimp_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"
    }]
  }
}

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
  }'
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 (paginado)

GET
/timbrado/cfdi/{uuid}

Detalle de un CFDI

GET
/timbrado/cfdi/{uuid}/xml

Descargar XML timbrado

GET
/timbrado/cfdi/{uuid}/pdf

Descargar PDF generado

GET
/timbrado/cfdi/{uuid}/html

Vista HTML del CFDI

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

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

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+)

Próximamente. Los webhooks estarán disponibles para el plan Pro+. Recibirás notificaciones en tu servidor cuando ocurran eventos como timbre.created, timbre.cancelled y batch.completed, con verificación HMAC-SHA256.

Mientras tanto, puedes consultar el estado de tus timbres con GET /timbrado/cfdi/{uuid} o listar con GET /timbrado/timbres.

Catálogos SAT

60+ catálogos SAT actualizados. Úsalos para autocompletados y validaciones en tu frontend.

GET
/catalogos/{nombre}

Consultar catálogo por nombre

forma-pagometodo-pagomonedaregimen-fiscaluso-cfdiproducto-serviciounidadtipo-comprobantetipo-relacionimpuestoestado+50 más
Ejemplobash
curl https://lite.senhub.mx/api/v1/catalogos/regimen-fiscal \
  -H "Authorization: Bearer $TOKEN"

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-08-01.1.

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)30 minutos
Refresh token7 días (renueva el JWT 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_environmenteste 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_csdtrue si ese RFC es un CSD de prueba del SAT.
  • sandbox_forcedtrue cuando no es posible timbrar en real con esa combinación de credencial y emisor.
  • sandbox_forced_reasonapi_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":"[email protected]"}'
2

Verificar OTP

curl -X POST https://lite.senhub.mx/api/v1/auth/otp/verify \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","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 "[email protected]" -F "[email protected]" -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?

Ilimitados. Sube N certificados CSD y timbra para cualquiera.

¿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, con una suscripción activa Starter o superior. La llave se muestra una sola vez y es exclusivamente server-to-server: nunca la uses desde el navegador.

¿Qué pasa si un timbre falla?

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

¿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+ puedes personalizar branding. En Free/Starter usa template 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.

¿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.

[email protected] · senhub.mx/developers