API de Numy
API REST para automatizar trámites fiscales mexicanos de forma programática: facturar una
compra desde el portal del comercio, emitir tus propios CFDI,
validar pagos SPEI contra el CEP de Banxico, y descargar del SAT tu
Constancia de Situación Fiscal, tu Opinión de Cumplimiento (32-D)
y tu Buzón Tributario. La mayoría de las operaciones son asíncronas:
respondemos con un job_id, consultas el estado por polling y/o
recibes el resultado por webhook.
Base URL
https://api.numy.mx
Autenticación
Todas las rutas bajo /api/v1/ requieren una API Key
con formato numy_sk_.... Se puede enviar de dos formas:
| Método | Ejemplo |
|---|---|
Header X-API-Key |
X-API-Key: numy_sk_abc123... |
| Bearer token | Authorization: Bearer numy_sk_abc123... |
Las rutas de gestión de webhooks (/api/me/webhook-endpoints) usan autenticación
Sanctum (cookie de sesión o Bearer token de Sanctum).
Flujo General
El flujo de facturación es asincrónico. La API acepta tu solicitud y la procesa en segundo plano:
/api/v1/invoicesEnviar ticket + datos fiscales → 202 Accepted
/api/v1/invoices/{id}(Opcional) Polling del estado del job
invoice.completed / invoice.failedNotificación automática a tu webhook con el resultado
Endpoints
1. Crear solicitud de factura
Solicita la factura (CFDI) de una compra a partir de la foto de un ticket: Numy
obtiene el comprobante desde el portal de facturación del comercio por ti — aquí
tú eres el comprador. Es asíncrono: respondemos con un
job_id y el resultado llega por webhook o polling.
¿Eres tú quien vende y necesitas emitir el CFDI a tu cliente?
Ese es otro flujo → Emitir CFDI propio.
Headers
X-API-Key: numy_sk_xxxxx Content-Type: application/json
Body
{
"ticket_image_url": "https://example.com/ticket.jpg",
"fiscal_data": {
"rfc": "XAXX010101000",
"person_type": "fisica",
"first_name": "Juan",
"last_name": "Pérez",
"mothers_last_name": "López",
"tax_regime": "626",
"cfdi_usage": "G03",
"tax_zip_code": "06600",
"email": "juan@example.com"
},
"metadata": {
"external_id": "order-123",
"client_ref": "abc"
}
}
Campos del body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
ticket_image_url |
string | Sí | URL pública de la imagen del ticket (JPEG, PNG, WebP). Máx 10 MB. |
fiscal_data |
object | Sí | Datos fiscales del receptor |
fiscal_data.rfc |
string | Sí | RFC del receptor (13 chars persona física, 12 chars persona moral) |
fiscal_data.person_type |
string | Sí | "fisica" o "moral"
|
fiscal_data.first_name |
string | Física | Nombre(s) — requerido para persona física |
fiscal_data.last_name |
string | Física | Apellido paterno — requerido para persona física |
fiscal_data.mothers_last_name |
string | No | Apellido materno (opcional) |
fiscal_data.legal_name |
string | Moral | Razón social — requerido para persona moral |
fiscal_data.tax_regime |
string | Sí | Clave del régimen fiscal (ver catálogos) |
fiscal_data.cfdi_usage |
string | Sí | Clave del uso de CFDI (ver catálogos) |
fiscal_data.tax_zip_code |
string | Sí | Código postal fiscal (5 dígitos) |
fiscal_data.email |
string | Sí | Email donde se envía la factura |
metadata |
object | No | Datos libres para correlación (máx 10 keys, valores string, máx 500 chars c/u) |
Respuesta exitosa — 202 Accepted
{
"job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
"status": "queued",
"message": "Solicitud recibida. Recibirás el resultado en tu webhook.",
"estimated_time_seconds": 120,
"created_at": "2026-03-28T12:00:00.000000Z",
"metadata": { "external_id": "order-123" }
}
Errores posibles
| HTTP | Código | Descripción |
|---|---|---|
| 401 | missing_api_key |
No se envió API Key |
| 401 | invalid_api_key |
API Key inválida o revocada |
| 402 | insufficient_tickets |
No hay tickets disponibles (incluye tickets_remaining y upgrade_url) |
| 422 | validation_error |
Errores de validación (incluye details con campos
específicos) |
Ejemplo — Error 422:
{
"error": "Validation failed",
"code": "validation_error",
"details": {
"fiscal_data.rfc": ["El RFC no tiene un formato válido."],
"fiscal_data.tax_regime": ["El régimen fiscal no corresponde al tipo de persona."]
}
}
Ejemplo — Error 402:
{
"error": "No hay tickets disponibles en tu suscripción.",
"code": "insufficient_tickets",
"tickets_remaining": 0,
"upgrade_url": "https://console.numy.mx/planes"
}
2. Consultar estado de solicitud
Consulta el estado de una solicitud de factura usando el job_id que
recibiste al crearla (polling). Útil como alternativa o respaldo del webhook.
Headers
X-API-Key: numy_sk_xxxxx
Respuestas según estado
queued — En cola:
{
"job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
"status": "queued",
"message": "Tu solicitud está en cola. Recibirás el resultado en tu webhook.",
"created_at": "2026-03-28T12:00:00.000000Z",
"completed_at": null,
"metadata": { "external_id": "order-123" },
"estimated_time_seconds": 120
}
processing — En proceso:
{
"job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
"status": "processing",
"message": "Tu factura está siendo procesada.",
"created_at": "2026-03-28T12:00:00.000000Z",
"completed_at": null,
"metadata": { "external_id": "order-123" },
"estimated_time_seconds": 120
}
success — Completada:
{
"job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
"status": "success",
"created_at": "2026-03-28T12:00:00.000000Z",
"completed_at": "2026-03-28T12:02:15.000000Z",
"metadata": { "external_id": "order-123" },
"estimated_time_seconds": 120,
"result": {
"comercio": "Oxxo",
"folio": "ABC123",
"total": "152.50",
"files": {
"pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx",
"xml_url": "https://api.numy.mx/api/dl/tok_xxxxx"
},
"download_page_url": "https://api.numy.mx/api/factura/tok_xxxxx"
}
}
failed — Error:
{
"job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
"status": "failed",
"created_at": "2026-03-28T12:00:00.000000Z",
"completed_at": "2026-03-28T12:00:05.000000Z",
"metadata": { "external_id": "order-123" },
"estimated_time_seconds": 120,
"error": {
"code": "vision_failed",
"message": "No se pudo leer el ticket. La imagen puede estar borrosa o dañada."
}
}
3. Validar pago SPEI (CEP de Banxico)
Valida de forma asíncrona que un pago SPEI se haya liquidado, consultando el CEP del
Banco de México. Entrada por datos estructurados (sin imagen). El resultado llega por
webhook payment.completed / payment.failed
o por polling.
Headers
X-API-Key: numy_sk_xxxxx Content-Type: application/json
Body
{
"tracking_key": "ABCD1234567890",
"operation_date": "2026-05-20",
"amount": "1500.00",
"sender_bank": "BBVA",
"receiver_bank": "STP",
"beneficiary_account": "012345678901234567",
"is_bank_payment": false,
"metadata": { "external_id": "pago-001" }
}
Campos del body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
tracking_key |
string | Sí* | Clave de rastreo (7–30 caracteres). *Requerido si no envías reference_number. |
reference_number |
string | Sí* | Número de referencia (≤7 dígitos). Alternativa a la clave de rastreo. |
operation_date |
string | Sí | Fecha del pago YYYY-MM-DD. Banxico solo conserva ~45
días hábiles. |
amount |
number | Sí | Monto exacto del pago. |
receiver_bank |
string | Sí | Banco receptor: nombre o clave SPEI de 5 dígitos. |
sender_bank |
string | No | Banco emisor: nombre o clave SPEI. |
beneficiary_account |
string | Sí | CLABE (18), tarjeta (16) o celular (10 dígitos). |
is_bank_payment |
boolean | No | Marca "Pago a banco" en el CEP. Default false. |
metadata |
object | No | Datos libres para correlación. |
Respuesta — 202 Accepted
{
"job_id": "pay_aBcDeFgHiJkLmNoPqRsT",
"status": "queued",
"message": "Solicitud recibida. Recibirás el resultado en tu webhook.",
"estimated_time_seconds": 180,
"created_at": "2026-05-26T12:00:00.000000Z",
"metadata": { "external_id": "pago-001" }
}
Consulta el estado con GET /api/v1/requests/{job_id}. El resultado (cep_status: liquidado, en_proceso, no_encontrado…) y los archivos
del CEP llegan en el webhook payment.completed.
4. Descargar Constancia de Situación Fiscal
Descarga de forma asíncrona la Constancia de Situación Fiscal desde el portal del SAT
usando la e.firma. El PDF llega por webhook constancia.completed /
constancia.failed o por polling.
Seguridad: la e.firma (.cer, .key y contraseña) se envía
por multipart/form-data, se usa una sola vez y no se
almacena en Numy. Envíala siempre sobre HTTPS.
Body — multipart/form-data
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
cer |
file | Sí | Archivo .cer de la e.firma. |
key |
file | Sí | Archivo .key de la clave privada. |
password |
string | Sí | Contraseña de la clave privada. |
rfc |
string | Sí | RFC del contribuyente. |
Respuesta — 202 Accepted
{
"job_id": "con_aBcDeFgHiJkLmNoPqRsT",
"status": "queued",
"message": "Solicitud recibida. Recibirás la constancia en tu webhook.",
"estimated_time_seconds": 300,
"created_at": "2026-05-26T12:00:00.000000Z",
"metadata": null
}
El webhook constancia.completed incluye el resultado:
{
"event": "constancia.completed",
"job_id": "con_aBcDeFgHiJkLmNoPqRsT",
"status": "success",
"result": {
"rfc": "VAGL960809SQ0",
"constancia_pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx"
},
"completed_at": "2026-05-26T12:05:00.000000Z"
}
5. Descargar Opinión de Cumplimiento (forma 32-D)
Descarga de forma asíncrona la Opinión de Cumplimiento de Obligaciones Fiscales
(forma 32-D) del portal SAT con la e.firma del contribuyente. El PDF oficial llega por webhook
opinion_cumplimiento.completed /
opinion_cumplimiento.failed o por polling. El resultado incluye el
dictamen extraído (Positiva, Negativa, Inscrito sin obligaciones, Suspensión de actividades — Regla
2.1.36 RMF 2026).
Seguridad: la e.firma (.cer, .key y contraseña) se envía
por multipart/form-data, se usa una sola vez y no se
almacena en Numy. Envíala siempre sobre HTTPS.
Body — multipart/form-data
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
cer |
file | Sí | Archivo .cer de la e.firma. |
key |
file | Sí | Archivo .key de la clave privada. |
password |
string | Sí | Contraseña de la clave privada. |
rfc |
string | Sí | RFC del contribuyente. PF (13 chars) y PM (12 chars) abren URLs distintas del portal. |
Respuesta — 202 Accepted
{
"job_id": "opn_aBcDeFgHiJkLmNoPqRsT",
"status": "queued",
"message": "Solicitud recibida. Recibirás la Opinión de Cumplimiento en tu webhook.",
"estimated_time_seconds": 300,
"created_at": "2026-05-27T12:00:00.000000Z",
"metadata": null
}
El webhook opinion_cumplimiento.completed incluye el resultado:
{
"event": "opinion_cumplimiento.completed",
"job_id": "opn_aBcDeFgHiJkLmNoPqRsT",
"status": "success",
"result": {
"rfc": "VAGL960809SQ0",
"dictamen": "Positiva",
"opinion_pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx"
},
"completed_at": "2026-05-27T12:05:00.000000Z"
}
Estados posibles del dictamen (Regla 2.1.36 RMF 2026):
"Positiva"— al corriente."Negativa"— con incumplimientos."Inscrito sin obligaciones fiscales"— RFC sin obligaciones periódicas."Suspensión de actividades"— RFC suspendido.null— el agente bajó el PDF pero no se pudo extraer el dictamen (el PDF está intacto, solo es metadata adicional).
Vigencia del documento: 30 días naturales en general, 90 días para subsidios y estímulos fiscales.
6. Consultar Buzón Tributario (modo solo lectura)
Consulta de forma asíncrona el Buzón Tributario del SAT del contribuyente. Devuelve
los listados de las bandejas (Pendientes, Notificados, Comunicados, Documentos) más PDFs descargados
de comunicados y documentos. El resultado llega por webhook
buzon_tributario.completed /
buzon_tributario.failed.
Salvaguarda legal — solo lectura: este endpoint NUNCA abre notificaciones en la bandeja "Pendientes". Abrirlas dispara plazos legales contra el contribuyente bajo CFF arts. 17-K y 134-I (jurisprudencia SCJN Comunicado 013/2024). Numy enumera los pendientes para que tú decidas cuándo abrirlos desde el portal SAT con tu e.firma.
Body — multipart/form-data
Mismos campos que /api/v1/opinion-cumplimiento: cer, key, password, rfc, metadata (opcional).
Respuesta — 202 Accepted
{
"job_id": "bzn_aBcDeFgHiJkLmNoPqRsT",
"status": "queued",
"message": "Solicitud recibida. El listado de tu Buzón llegará a tu webhook (modo solo lectura, sin abrir notificaciones).",
"estimated_time_seconds": 360,
"created_at": "2026-05-27T12:00:00.000000Z",
"metadata": null
}
El webhook buzon_tributario.completed incluye los listados completos:
{
"event": "buzon_tributario.completed",
"job_id": "bzn_aBcDeFgHiJkLmNoPqRsT",
"status": "success",
"result": {
"rfc": "VAGL960809SQ0",
"buzon_habilitado": true,
"resumen": {
"pendientes": 2,
"comunicados": 3,
"documentos": 5
},
"notificaciones_pendientes": [
{ "folio": "N001", "fecha_aviso": "2026-05-25", "asunto": "Requerimiento", "remitente": "SAT" }
],
"comunicados": [
{ "folio": "C001", "fecha": "2026-05-20", "asunto": "Recordatorio", "pdf_path": "..." }
],
"documentos": [
{ "folio": "D001", "fecha": "2026-05-15", "tipo": "Acuse", "pdf_path": "..." }
],
"buzon_url": "https://api.numy.mx/api/buzon/abc123token",
"expires_at": "2026-06-03T12:00:00+00:00"
},
"completed_at": "2026-05-27T12:06:00.000000Z"
}
Notas clave:
buzon_habilitado: falsecuando el SAT reporta que el Buzón no está activado (caso típico: PF RESICO antes del 1-ene-2027). En ese caso los listados llegan vacíos.buzon_urlsirve a un endpoint público sin auth (token URL-safe, expira 7 días) con los PDFs descargados.- Si el agente IA intenta abrir una notificación pendiente, el job falla con código
intent_blocked_open_pendiente(no reintentable).
Disclaimer legal: las notificaciones del Buzón tienen efectos jurídicos bajo CFF arts. 17-K y 134-I. Tienes 3 días hábiles desde el aviso para abrirlas; al cuarto día se tienen por notificadas. Numy NO las abre por ti — solo te informa que existen.
7. Emitir CFDI
Emite un CFDI de Ingreso (tipo I) como emisor/vendedor, de forma síncrona: el comprobante timbrado se devuelve en la misma respuesta (no usa webhook). ¿Solo quieres la factura de algo que compraste? Ese es otro flujo → Facturar una compra.
Modo prueba: la emisión opera en el entorno
sandbox de FacturAPI. Los CFDI generados NO tienen validez fiscal — la
respuesta incluye test_mode: true.
Body
{
"cliente_rfc": "XAXX010101000",
"cliente_nombre": "JOHN DOE",
"cliente_cp": "83240",
"cliente_regimen_codigo": "616",
"cliente_uso_codigo": "S01",
"cliente_email": "cliente@ejemplo.com",
"forma_pago_codigo": "08",
"metodo_pago_codigo": "PUE",
"conceptos": [
{
"descripcion": "Servicio de consultoría",
"cantidad": 1,
"valor_unitario": 1000,
"precio_con_iva": false,
"tipo_iva": "16"
}
],
"metadata": { "external_id": "cfdi-001" }
}
Campos del body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
cliente_rfc |
string | Sí | RFC del receptor. |
cliente_nombre |
string | Sí | Nombre o razón social del receptor. |
cliente_cp |
string | Sí | Código postal fiscal (5 dígitos). |
cliente_regimen_codigo |
string | Sí | Régimen fiscal del receptor (ver catálogos). |
cliente_uso_codigo |
string | Sí | Uso de CFDI (ver catálogos). |
cliente_email |
string | No | Email del receptor. |
forma_pago_codigo |
string | No | Forma de pago SAT (default 01). |
metodo_pago_codigo |
string | No | PUE o PPD (default
PUE). |
conceptos[] |
array | Sí | Al menos 1 concepto. |
conceptos[].descripcion |
string | Sí | Descripción del concepto. |
conceptos[].valor_unitario |
number | Sí | Precio unitario (mayor a 0). |
conceptos[].precio_con_iva |
boolean | Sí | true si el precio ya incluye IVA, false si es neto. |
conceptos[].cantidad |
number | No | Default 1. |
conceptos[].clave_prodserv |
string | No | Clave de producto/servicio SAT (default genérica). |
conceptos[].clave_unidad |
string | No | Clave de unidad SAT (default E48). |
conceptos[].tipo_iva |
string | No | 16, 8, 0 o exento (default
16). |
Respuesta — 200 OK
{
"uuid": "bb56f134-8427-4664-8d0f-7e3d8f83fea2",
"folio": "1",
"series": "F",
"total": 1160.00,
"currency": "MXN",
"verification_url": "https://verificacfdi.facturaelectronica.sat.gob.mx/...",
"livemode": false,
"test_mode": true,
"duplicate": false,
"downloads": [
{ "type": "pdf", "url": "https://api.numy.mx/api/dl/tok_xxxxx" },
{ "type": "xml", "url": "https://api.numy.mx/api/dl/tok_xxxxx" }
],
"metadata": { "external_id": "cfdi-001" }
}
Errores posibles
| HTTP | Código | Descripción |
|---|---|---|
| 422 | validation_error / invalid_payload |
Datos faltantes o inválidos. No consume ticket. |
| 422 | tax_ambiguous |
Falta definir precio_con_iva en algún concepto. |
| 402 | insufficient_tickets |
No hay tickets disponibles. |
| 502 | facturapi_error |
Error del proveedor de timbrado. |
Cobro: 1 emisión = 1 ticket. Un payload inválido (422) no consume; una emisión que llega a FacturAPI consume aunque sea rechazada; un duplicado idempotente reusa el resultado previo sin volver a cobrar.
Webhooks
Configuración
Los webhooks se gestionan bajo /api/me/webhook-endpoints con
autenticación Sanctum.
Crear webhook endpoint
{
"url": "https://tu-servidor.com/webhook",
"events": ["invoice.completed", "invoice.failed"],
"description": "Producción"
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
url |
string | Sí | URL HTTPS donde recibir los webhooks |
events |
string[] | No | Filtrar por eventos. null = todos los eventos |
description |
string | No | Etiqueta descriptiva |
Eventos disponibles: invoice.completed, invoice.failed, payment.completed,
payment.failed, constancia.completed,
constancia.failed,
opinion_cumplimiento.completed,
opinion_cumplimiento.failed,
buzon_tributario.completed,
buzon_tributario.failed,
webhook.test
Respuesta 201 Created:
{
"webhook_endpoint": {
"id": 1,
"url": "https://tu-servidor.com/webhook",
"events": ["invoice.completed", "invoice.failed"],
"is_active": true,
"description": "Producción",
"last_triggered_at": null,
"created_at": "2026-03-28T10:00:00.000000Z"
},
"secret": "whsec_aBcDeFgHiJkLmNoPqRsTuVwXyZ...",
"warning": "Guarda este secret de forma segura. No podrás verlo de nuevo."
}
Importante: El secret solo se muestra una vez al crear el endpoint. Guárdalo de
forma segura.
Listar webhook endpoints
{
"data": [
{
"id": 1,
"url": "https://tu-servidor.com/webhook",
"events": ["invoice.completed", "invoice.failed"],
"is_active": true,
"description": "Producción",
"last_triggered_at": "2026-03-28T12:05:00.000000Z",
"created_at": "2026-03-28T10:00:00.000000Z"
}
]
}
Actualizar webhook endpoint
{
"url": "https://nuevo-servidor.com/webhook",
"events": ["invoice.completed"],
"is_active": false,
"description": "Staging"
}
Eliminar webhook endpoint
Respuesta: 204 No Content
Rotar secret
{
"webhook_endpoint": { ... },
"secret": "whsec_NuEvOsEcReTaQuI...",
"warning": "Guarda este nuevo secret de forma segura. No podrás verlo de nuevo."
}
Enviar webhook de prueba
Respuesta exitosa:
{ "success": true, "message": "Webhook de prueba enviado correctamente." }
Respuesta fallida (502):
{ "success": false, "message": "No se pudo entregar el webhook de prueba. Verifica la URL." }
Payloads de webhook
Numy envía un POST con body JSON a la URL configurada.
Headers del webhook
| Header | Descripción |
|---|---|
Content-Type |
application/json |
X-Numy-Signature |
sha256=<hmac_hex> — firma HMAC-SHA256 |
X-Numy-Timestamp |
Unix timestamp del envío |
User-Agent |
Numy-Webhook/1.0 |
Evento: invoice.completed
{
"event": "invoice.completed",
"job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
"status": "success",
"result": {
"comercio": "Oxxo",
"folio": "ABC123",
"total": "152.50",
"files": {
"pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx",
"xml_url": "https://api.numy.mx/api/dl/tok_xxxxx"
},
"download_page_url": "https://api.numy.mx/api/factura/tok_xxxxx"
},
"metadata": { "external_id": "order-123" },
"completed_at": "2026-03-28T12:02:15.000000Z"
}
Evento: invoice.failed
{
"event": "invoice.failed",
"job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
"status": "failed",
"error": {
"code": "vision_failed",
"message": "No se pudo leer el ticket. La imagen puede estar borrosa o dañada.",
"retryable": false
},
"metadata": { "external_id": "order-123" },
"failed_at": "2026-03-28T12:00:05.000000Z"
}
Evento: webhook.test
{
"event": "webhook.test",
"message": "Este es un webhook de prueba desde Numy.",
"timestamp": "2026-03-28T12:00:00.000000Z"
}
Códigos de error en eventos de webhook
| Código | Retryable | Descripción |
|---|---|---|
invalid_url |
No | URL de imagen inválida |
download_failed |
Sí | No se pudo descargar la imagen |
file_too_large |
No | La imagen excede 10 MB |
invalid_image_type |
No | Formato de imagen no soportado (usar JPEG, PNG, WebP) |
storage_failed |
Sí | Error interno al guardar la imagen |
vision_failed |
Sí | No se pudo analizar el ticket con OCR |
image_download_failed |
Sí | Error general de descarga de imagen |
processing_failed |
No | El ticket no pudo ser procesado automáticamente |
browser_use_failed |
Sí | Error durante la automatización del portal |
browser_use_timeout |
Sí | Timeout durante la automatización |
retryable: true indica que enviar la misma imagen de nuevo podría
funcionar.
Verificación de firma
- Obtén el timestamp del header
X-Numy-Timestamp - Obtén la firma del header
X-Numy-Signature(quitar el prefijosha256=) - Computa el HMAC:
HMAC-SHA256(timestamp + "." + body, SHA256(secret))- El
secretes el plain textwhsec_...recibido al crear el endpoint - Primero hashea el secret con SHA-256, luego úsalo como clave del HMAC
- El
- Compara tu HMAC con la firma recibida
Ejemplo en Node.js
const crypto = require('crypto');
function verifyWebhook(body, signature, timestamp, secret) {
const signingKey = crypto.createHash('sha256').update(secret).digest('hex');
const expected = crypto
.createHmac('sha256', signingKey)
.update(`${timestamp}.${body}`)
.digest('hex');
const received = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex')
);
}
Ejemplo en PHP
function verifyWebhook(string $body, string $signature, string $timestamp, string $secret): bool
{
$signingKey = hash('sha256', $secret);
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $signingKey);
$received = str_replace('sha256=', '', $signature);
return hash_equals($expected, $received);
}
Tip de seguridad: Valida que el timestamp no tenga más de 5 minutos de diferencia para prevenir ataques de replay.
Reintentos y desactivación
- Numy intenta entregar cada webhook hasta 4 veces (1 intento + 3 reintentos).
- Intervalos: 5s, 30s, 120s entre reintentos.
- Si todos los intentos fallan, el endpoint se desactiva automáticamente (
is_active: false). - El endpoint se puede reactivar manualmente via
PATCH.
Tu servidor debe responder con un código 2xx dentro de 10 segundos para confirmar la recepción.
Catálogos Fiscales
Regímenes fiscales — Persona Física
| Clave | Descripción |
|---|---|
605 |
Sueldos y Salarios e Ingresos Asimilados a Salarios |
606 |
Arrendamiento |
607 |
Régimen de Enajenación o Adquisición de Bienes |
608 |
Demás ingresos |
611 |
Ingresos por Dividendos (socios y accionistas) |
612 |
Personas Físicas con Actividades Empresariales y Profesionales |
614 |
Ingresos por intereses |
615 |
Régimen de los ingresos por obtención de premios |
621 |
Incorporación Fiscal |
625 |
Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas |
626 |
Régimen Simplificado de Confianza |
Regímenes fiscales — Persona Moral
| Clave | Descripción |
|---|---|
601 |
General de Ley Personas Morales |
603 |
Personas Morales con Fines no Lucrativos |
609 |
Consolidación |
620 |
Sociedades Cooperativas de Producción |
622 |
Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras |
623 |
Opcional para Grupos de Sociedades |
624 |
Coordinados |
626 |
Régimen Simplificado de Confianza |
Uso de CFDI — Ambos tipos de persona
| Clave | Descripción |
|---|---|
G01 |
Adquisición de mercancías |
G02 |
Devoluciones, descuentos o bonificaciones |
G03 |
Gastos en general |
I01 |
Construcciones |
I02 |
Mobiliario y equipo de oficina por inversiones |
I03 |
Equipo de transporte |
I04 |
Equipo de cómputo y accesorios |
I05 |
Dados, troqueles, moldes, matrices y herramental |
I06 |
Comunicaciones telefónicas |
I07 |
Comunicaciones satelitales |
I08 |
Otra maquinaria y equipo |
S01 |
Sin efectos fiscales |
CP01 |
Pagos |
Uso de CFDI — Solo Persona Física (adicionales)
| Clave | Descripción |
|---|---|
D01 |
Honorarios médicos, dentales y gastos hospitalarios |
D02 |
Gastos médicos por incapacidad o discapacidad |
D03 |
Gastos funerales |
D04 |
Donativos |
D05 |
Intereses reales efectivamente pagados por créditos hipotecarios |
D06 |
Aportaciones voluntarias al SAR |
D07 |
Primas por seguros de gastos médicos |
D08 |
Gastos de transportación escolar obligatoria |
D09 |
Depósitos en cuentas para el ahorro, primas de pensiones |
D10 |
Pagos por servicios educativos (colegiaturas) |
CN01 |
Nómina |
Resumen de rutas
API Key auth (X-API-Key / Bearer numy_sk_...)
| Método | Ruta | Descripción |
|---|---|---|
| POST | /api/v1/invoices |
Facturar una compra: obtener el CFDI desde el portal del comercio (async) |
| GET | /api/v1/invoices/{job_id} |
Consultar estado de solicitud |
| POST | /api/v1/payment-validations |
Validar pago SPEI contra el CEP de Banxico (async) |
| POST | /api/v1/constancias |
Descargar Constancia de Situación Fiscal (async, e.firma) |
| POST | /api/v1/opinion-cumplimiento |
Descargar Opinión de Cumplimiento 32-D (async, e.firma) |
| POST | /api/v1/buzon-tributario |
Consultar Buzón Tributario en modo solo lectura (async, e.firma) |
| POST | /api/v1/cfdi |
Emitir CFDI propio como emisor/vendedor (síncrono, sandbox) |
| GET | /api/v1/requests/{job_id} |
Estado de solicitud async (pago, constancia, opinión, buzón) |
Sanctum auth (panel)
| Método | Ruta | Descripción |
|---|---|---|
| GET | /api/me/webhook-endpoints |
Listar webhooks |
| POST | /api/me/webhook-endpoints |
Crear webhook endpoint |
| PATCH | /api/me/webhook-endpoints/{id} |
Actualizar webhook |
| DELETE | /api/me/webhook-endpoints/{id} |
Eliminar webhook |
| POST | /api/me/webhook-endpoints/{id}/rotate-secret |
Rotar secret |
| POST | /api/me/webhook-endpoints/{id}/test |
Enviar webhook de prueba |
Ejemplos cURL
Facturar una compra
curl -X POST https://api.numy.mx/api/v1/invoices \
-H "X-API-Key: numy_sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"ticket_image_url": "https://storage.example.com/ticket-001.jpg",
"fiscal_data": {
"rfc": "PELL900101ABC",
"person_type": "fisica",
"first_name": "Juan",
"last_name": "Pérez",
"mothers_last_name": "López",
"tax_regime": "626",
"cfdi_usage": "G03",
"tax_zip_code": "06600",
"email": "juan@example.com"
},
"metadata": { "external_id": "order-456" }
}'
Consultar estado
curl https://api.numy.mx/api/v1/invoices/inv_aBcDeFgHiJkLmNoPqRsT \ -H "X-API-Key: numy_sk_xxxxx"
Crear webhook endpoint
curl -X POST https://api.numy.mx/api/me/webhook-endpoints \
-H "Authorization: Bearer {sanctum_token}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-servidor.com/numy-webhook",
"events": ["invoice.completed", "invoice.failed"],
"description": "Producción"
}'