API Reference

Invoice API

API para crear facturas de forma programática. Soporta envío de imagen de ticket + datos fiscales, procesamiento asincrónico, polling de estado y notificaciones via webhooks.

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

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}

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."
  }
}

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, 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 Crear solicitud de factura
GET /api/v1/invoices/{job_id} Consultar estado de solicitud

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

Crear factura

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"
  }'