API Reference

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.

dns

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:

1
POST/api/v1/invoices

Enviar ticket + datos fiscales → 202 Accepted

2
GET/api/v1/invoices/{id}

(Opcional) Polling del estado del job

3
WHinvoice.completed / invoice.failed

Notificación automática a tu webhook con el resultado

Endpoints

1. Crear solicitud de factura

POST/api/v1/invoices

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 URL pública de la imagen del ticket (JPEG, PNG, WebP). Máx 10 MB.
fiscal_data object Datos fiscales del receptor
fiscal_data.rfc string RFC del receptor (13 chars persona física, 12 chars persona moral)
fiscal_data.person_type string "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 Clave del régimen fiscal (ver catálogos)
fiscal_data.cfdi_usage string Clave del uso de CFDI (ver catálogos)
fiscal_data.tax_zip_code string Código postal fiscal (5 dígitos)
fiscal_data.email string 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

GET/api/v1/invoices/{job_id}

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)

POST/api/v1/payment-validations

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 Fecha del pago YYYY-MM-DD. Banxico solo conserva ~45 días hábiles.
amount number Monto exacto del pago.
receiver_bank string Banco receptor: nombre o clave SPEI de 5 dígitos.
sender_bank string No Banco emisor: nombre o clave SPEI.
beneficiary_account string 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

POST/api/v1/constancias

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 Archivo .cer de la e.firma.
key file Archivo .key de la clave privada.
password string Contraseña de la clave privada.
rfc string 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)

POST/api/v1/opinion-cumplimiento

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 Archivo .cer de la e.firma.
key file Archivo .key de la clave privada.
password string Contraseña de la clave privada.
rfc string 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)

POST/api/v1/buzon-tributario

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: false cuando 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_url sirve 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

POST/api/v1/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 RFC del receptor.
cliente_nombre string Nombre o razón social del receptor.
cliente_cp string Código postal fiscal (5 dígitos).
cliente_regimen_codigo string Régimen fiscal del receptor (ver catálogos).
cliente_uso_codigo string 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 Al menos 1 concepto.
conceptos[].descripcion string Descripción del concepto.
conceptos[].valor_unitario number Precio unitario (mayor a 0).
conceptos[].precio_con_iva boolean 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

POST/api/me/webhook-endpoints
{
  "url": "https://tu-servidor.com/webhook",
  "events": ["invoice.completed", "invoice.failed"],
  "description": "Producción"
}
Campo Tipo Requerido Descripción
url string 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

GET/api/me/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

PATCH/api/me/webhook-endpoints/{id}
{
  "url": "https://nuevo-servidor.com/webhook",
  "events": ["invoice.completed"],
  "is_active": false,
  "description": "Staging"
}

Eliminar webhook endpoint

DELETE/api/me/webhook-endpoints/{id}

Respuesta: 204 No Content

Rotar secret

POST/api/me/webhook-endpoints/{id}/rotate-secret
{
  "webhook_endpoint": { ... },
  "secret": "whsec_NuEvOsEcReTaQuI...",
  "warning": "Guarda este nuevo secret de forma segura. No podrás verlo de nuevo."
}

Enviar webhook de prueba

POST/api/me/webhook-endpoints/{id}/test

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 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 Error interno al guardar la imagen
vision_failed No se pudo analizar el ticket con OCR
image_download_failed Error general de descarga de imagen
processing_failed No El ticket no pudo ser procesado automáticamente
browser_use_failed Error durante la automatización del portal
browser_use_timeout Timeout durante la automatización

retryable: true indica que enviar la misma imagen de nuevo podría funcionar.

Verificación de firma

  1. Obtén el timestamp del header X-Numy-Timestamp
  2. Obtén la firma del header X-Numy-Signature (quitar el prefijo sha256=)
  3. Computa el HMAC: HMAC-SHA256(timestamp + "." + body, SHA256(secret))
    • El secret es el plain text whsec_... recibido al crear el endpoint
    • Primero hashea el secret con SHA-256, luego úsalo como clave del HMAC
  4. 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"
  }'