Referencia de API

MODO PRUEBA

Todos los cuerpos son JSON en snake_case; los campos en null se omiten de la respuesta (no aparecen como null, no aparecen). Los montos siempre son enteros en centavos (amount_minor), nunca flotantes.

GrupoAuthHeader adicional
/v1/*Authorization: Bearer sk_test_… / sk_live_…Idempotency-Key (UUID) en los POST que mueven dinero — ver detalle por endpoint.
/public/*Sin Authorization: el client_secret del intent autentica — en el cuerpo (POST) o en el header X-Winal-Client-Secret (GET; query ?client_secret= legado).
📄 El contrato completo, tipado
Cada endpoint de esta página también vive en el contrato OpenAPI (GET /openapi/v1.json): genera un cliente tipado en tu lenguaje o consúltalo desde tu IDE. Ver Genera tu cliente. Antes de integrar, revisa Entornos y Base URL y Autenticación y seguridad.

Paginación

Los endpoints de listado devuelven un envelope {"object": "list", "data": [...]}. Winal usa dos estilos de paginación según el endpoint; ambos son consistentes en su familia.

Cursor por id (exclusivo) — el flujo de eventos

GET /v1/events pagina con un cursor entero exclusivo: after_id devuelve solo filas con id > after_id. Avanzas tu cursor al id más alto que viste y repites; nunca repite ni salta eventos. Ver Eventos y polling.

ParámetroTipoDefault · tope
after_idint64, opcional0 (desde el primer evento); cursor exclusivo
limitint, opcional50 · tope 200 (valores mayores se recortan, no fallan)

Cursor opaco con has_more — listados de reportes

Los listados paginados de Reportes (p. ej. GET /v1/reports/payments) devuelven además has_more y next_cursor. Sigue pidiendo con ?cursor=<next_cursor> mientras has_more sea true; cuando es false, terminaste y next_cursor se omite. El cursor es opaco: pásalo tal cual, no lo construyas.

200 · página con más resultados
{
  "object": "list",
  "data": [ /* … filas … */ ],
  "has_more": true,
  "next_cursor": "eyJpZCI6MTI4LCJ0cyI6..."
}
ParámetroTipoDefault · tope
cursorstring opaco, opcionalel next_cursor de la página anterior; un cursor corrupto responde reports.invalid_cursor
limitint, opcionalvaría por endpoint (típico 20, tope 100; los listados admin usan default 100, tope 500) — siempre se recorta al tope

Los listados simples (sin cursor) devuelven todo el conjunto del tenant en data y aceptan ?limit= para acotar; no traen has_more. Cada endpoint indica su estilo en su fila de la Referencia.

Límites de solicitudes

El API aplica rate limiting por ventana fija de 1 minuto. Al excederlo recibes 429 con el envelope de error estándar y un header Retry-After.

TráficoSe cuenta porLímite por minuto (default)
/v1/* autenticadotu llave sk_…300
/public/*IP de cliente confiable60

Exentos (no consumen cupo): /health, /metrics, /status, /portal, /demo y /js/*. Los límites son configurables por despliegue, así que trata los valores de arriba como el default, no como un contrato duro.

429 · respuesta
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Se excedió el límite de solicitudes; reintenta más tarde."
  }
}
Qué headers hay (y cuáles no)
Ante un 429, respeta siempre el header Retry-After (segundos): espera ese tiempo y reintenta con backoff. Hoy Winal no emite headers X-RateLimit-Limit/X-RateLimit-Remaining; no cuentes con ellos — reintenta guiándote por Retry-After. La idempotencia (Idempotency-Key) hace que reintentar un POST que mueve dinero sea seguro, sin doble cargo.

Payment Intents

POST/v1/payment_intents

Crea un intent en requires_payment_method con un client_secret nuevo.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

amount_minorint64, requerido, > 0
currencystring ISO 4217, requerido (hoy solo MXN funciona con Sim)
payment_method_typesstring[], opcional — informativo: solo se registra en el historial, no fija el método real ni se guarda en el intent. El método que de verdad se usa es el que mandas en confirm.
metadataobject<string,string>, opcional
tip_minorint64, opcional, no negativo. null/omitido = 0. Caso típico del POS: se omite aquí y se fija después en confirm (ver Reportes → Propinas).

Errores posibles: payment_intent.invalid_amount, payment_intent.invalid_currency, payment_intent.invalid_tip (ver Errores).

request
POST /v1/payment_intents
Authorization: Bearer sk_test_...
Idempotency-Key: 6a1e3b2c-...
Content-Type: application/json

{ "amount_minor": 84900, "currency": "MXN" }
201
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 0,
  "total_minor": 84900,
  "currency": "MXN",
  "status": "requires_payment_method",
  "client_secret": "pi_secret_9fZ3kQ7bV1x...",
  "livemode": false,
  "created_at": "2026-07-05T18:30:00Z",
  "updated_at": "2026-07-05T18:30:00Z"
}
GET/v1/payment_intents/{id}

Lee un intent. Con ?expand=attempts incluye el historial de intentos de cobro.

Authorizationrequerido

Errores posibles: payment_intent.not_found.

request
GET /v1/payment_intents/5b6b8b3e-...?expand=attempts
Authorization: Bearer sk_test_...
200
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 0,
  "total_minor": 84900,
  "currency": "MXN",
  "status": "succeeded",
  "client_secret": "pi_secret_9fZ3kQ7bV1x...",
  "livemode": false,
  "created_at": "2026-07-05T18:30:00Z",
  "updated_at": "2026-07-05T18:30:04Z",
  "attempts": [
    {
      "id": "a13fce02-...",
      "object": "attempt",
      "status": "captured",
      "connector_key": "sim",
      "method": "card",
      "provider_ref": "sim_charge_9c1f...",
      "created_at": "2026-07-05T18:30:01Z"
    }
  ]
}
POST/v1/payment_intents/{id}/confirm

Confirma el cobro: valida ruteo, crea el attempt y encola su ejecución. La respuesta HTTP siempre llega con status: "processing" — el cobro se ejecuta después, fuera del request (regla: processing solo se resuelve con evidencia del proveedor). El resultado final llega por webhook o por un GET posterior.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

payment_tokenstring, requerido (tok_sim_* en pruebas)
payment_methodstring, requerido: card | spei | codi | dimo | oxxo
tip_minorint64, opcional, no negativo. Si se manda, reemplaza la propina que el intent ya tuviera; null/omitido deja la existente sin tocar. La respuesta trae tip_minor y total_minor (= amount_minor + tip_minor) — ver Reportes → Propinas.

Errores posibles: payment_intent.unauthorized, payment_intent.not_confirmable, payment_intent.no_route, payment_intent.concurrent_modification, payment_intent.invalid_tip.

request
POST /v1/payment_intents/5b6b8b3e-.../confirm
Authorization: Bearer sk_test_...
Idempotency-Key: 9d2f1a4e-...
Content-Type: application/json

{ "payment_token": "tok_sim_ok", "payment_method": "card", "tip_minor": 5000 }
200 · respuesta inmediata
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 5000,
  "total_minor": 89900,
  "currency": "MXN",
  "status": "processing",
  "client_secret": "pi_secret_9fZ3kQ7bV1x...",
  "livemode": false,
  "created_at": "2026-07-05T18:30:00Z",
  "updated_at": "2026-07-05T18:30:00Z",
  "attempts": [
    {
      "id": "a13fce02-...",
      "object": "attempt",
      "status": "pending",
      "connector_key": "sim",
      "method": "card",
      "created_at": "2026-07-05T18:30:00Z"
    }
  ]
}
POST/v1/payment_intents/{id}/capture

Captura un intento previamente autorizado sin capturar (flujo auth/capture del POS, p. ej. tras confirmar con tok_sim_auth). Sin cuerpo. El intent no cambia de estado en la respuesta — sigue processing hasta que el proveedor confirme la captura.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Errores posibles: attempt.not_capturable (no hay intento authorized).

request
POST /v1/payment_intents/5b6b8b3e-.../capture
Authorization: Bearer sk_test_...
Idempotency-Key: 2c3e9f10-...
200
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 0,
  "total_minor": 84900,
  "currency": "MXN",
  "status": "processing",
  "client_secret": "pi_secret_9fZ3kQ7bV1x...",
  "livemode": false,
  "created_at": "2026-07-05T18:30:00Z",
  "updated_at": "2026-07-05T18:31:10Z",
  "attempts": [
    { "id": "a13fce02-...", "object": "attempt", "status": "authorized",
      "connector_key": "sim", "method": "card",
      "provider_ref": "sim_auth_7b2c...", "created_at": "2026-07-05T18:30:00Z" }
  ]
}
POST/v1/payment_intents/{id}/cancel

Cancela el intent (si la máquina de estados lo permite). Sin cuerpo. Cancelar un intent ya canceled es un no-op que devuelve 200.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Errores posibles: transición inválida si el intent ya está en un estado terminal distinto (succeeded/failed/expired).

request
POST /v1/payment_intents/5b6b8b3e-.../cancel
Authorization: Bearer sk_test_...
Idempotency-Key: 77aa1c3e-...
200
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 0,
  "total_minor": 84900,
  "currency": "MXN",
  "status": "canceled",
  "client_secret": "pi_secret_9fZ3kQ7bV1x...",
  "livemode": false,
  "created_at": "2026-07-05T18:30:00Z",
  "updated_at": "2026-07-05T18:32:00Z"
}

Refunds

POST/v1/refunds

Crea una devolución en requested sobre un intento capturado.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

attempt_iduuid, requerido — el intento a devolver (no el payment_intent).
amount_minorint64, opcional. Si se omite, devuelve el saldo restante (cobrado menos devoluciones vivas) — no el monto original del cargo.
reasonstring, opcional

Errores posibles: ver la tabla de refunds en Errores.

request
POST /v1/refunds
Authorization: Bearer sk_test_...
Idempotency-Key: f1e2d3c4-...
Content-Type: application/json

{ "attempt_id": "a13fce02-...", "amount_minor": 84900, "reason": "devolución POS" }
201
{
  "id": "d4c5b6a7-...",
  "object": "refund",
  "attempt_id": "a13fce02-...",
  "amount_minor": 84900,
  "currency": "MXN",
  "status": "requested",
  "reason": "devolución POS",
  "created_at": "2026-07-05T19:00:00Z"
}
GET/v1/refunds/{id}

Lee una devolución por su id.

Authorizationrequerido

Errores posibles: refund.not_found.

request
GET /v1/refunds/d4c5b6a7-...
Authorization: Bearer sk_test_...
200
{
  "id": "d4c5b6a7-...",
  "object": "refund",
  "attempt_id": "a13fce02-...",
  "amount_minor": 84900,
  "currency": "MXN",
  "status": "succeeded",
  "provider_ref": "sim_refund_1a2b...",
  "reason": "devolución POS",
  "created_at": "2026-07-05T19:00:00Z"
}

Clientes (card-on-file)

Guía narrativa completa (activación asíncrona del método, cobro 1-click) en Clientes.

POST/v1/customers

Crea un cliente. Cuerpo vacío permitido.

Authorizationrequerido

Cuerpo

namestring, opcional
emailstring, opcional
metadataobject<string,string>, opcional
201 · respuesta real
{
  "id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
  "object": "customer",
  "name": "Juan Pérez",
  "email": "juan.perez@example.mx",
  "livemode": false,
  "created_at": "2026-07-08T02:18:51.369868+00:00",
  "updated_at": "2026-07-08T02:18:51.369868+00:00"
}
GET/v1/customers · GET/v1/customers/{id}

Lista o lee un cliente por su id.

Authorizationrequerido

Errores posibles: customer.not_found.

200
{ "object": "list", "data": [ { "id": "7c17884b-...", "object": "customer", "...": "..." } ] }
POST/v1/customers/{id}/payment_methods

Guarda un método de pago tokenizado. Nace pending; el Worker lo activa fuera del request.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

payment_tokenstring, requerido — token de un solo uso (tok_sim_* en pruebas)
connectorstring, opcional

Errores posibles: payment_method.invalid_token, payment_method.no_route, customer.not_found.

201 · respuesta real (pending)
{
  "id": "2a3c754f-c77a-4de0-9cf4-29aca3ff333e",
  "object": "payment_method",
  "customer_id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
  "connector": "sim",
  "status": "pending",
  "livemode": false,
  "created_at": "2026-07-08T02:19:31.043184+00:00",
  "updated_at": "2026-07-08T02:19:31.043184+00:00"
}
200 · GET /v1/payment_methods/{id}, segundos después (active)
{
  "id": "2a3c754f-c77a-4de0-9cf4-29aca3ff333e",
  "object": "payment_method",
  "customer_id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
  "connector": "sim",
  "brand": "visa",
  "last4": "1764",
  "status": "active",
  "livemode": false,
  "created_at": "2026-07-08T02:19:31.043184+00:00",
  "updated_at": "2026-07-08T02:19:31.367513+00:00"
}
GET/v1/customers/{id}/payment_methods

Lista los métodos guardados del cliente (incluye detached).

Authorizationrequerido
DELETE/v1/payment_methods/{id}

Transición terminal a detached. Idempotente: repetir sobre uno ya detached no falla.

Authorizationrequerido

Errores posibles: payment_method.not_found.

200 · respuesta real
{
  "id": "2a3c754f-c77a-4de0-9cf4-29aca3ff333e",
  "object": "payment_method",
  "customer_id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
  "connector": "sim",
  "brand": "visa",
  "last4": "1764",
  "status": "detached",
  "livemode": false,
  "created_at": "2026-07-08T02:19:31.043184+00:00",
  "updated_at": "2026-07-08T02:19:46.231193+00:00"
}

Cobro 1-click: manda payment_method_id en POST /v1/payment_intents/{id}/confirm en vez de payment_token + payment_method — ver Clientes para el ejemplo completo. Errores posibles propios de esa variante: payment_method.not_chargeable, payment_method.concurrent_modification.

Webhook Endpoints

POST/v1/webhook_endpoints

Registra un endpoint para recibir entregas. El secret solo se devuelve aquí.

Authorizationrequerido
Nota: a diferencia de los endpoints anteriores, esta ruta no pasa por el middleware de idempotencia — Idempotency-Key es opcional aquí y, si lo mandas, se ignora.

Cuerpo

urlstring HTTPS, requerido
eventsstring[], opcional — lista blanca de event_type; vacío/omitido = todo el catálogo (ver Webhooks)
request
POST /v1/webhook_endpoints
Authorization: Bearer sk_test_...
Content-Type: application/json

{ "url": "https://tu-servidor.mx/webhooks/winal" }
201
{
  "id": "c1a9f2e0-...",
  "object": "webhook_endpoint",
  "url": "https://tu-servidor.mx/webhooks/winal",
  "secret": "whsec_8Kx9..."
}
GET/v1/webhook_endpoints

Lista los endpoints del tenant. Nunca incluye secretos.

Authorizationrequerido
200
{
  "object": "list",
  "data": [
    {
      "id": "c1a9f2e0-...",
      "object": "webhook_endpoint",
      "url": "https://tu-servidor.mx/webhooks/winal",
      "active": true,
      "created_at": "2026-07-05T18:00:00Z"
    }
  ]
}
DELETE/v1/webhook_endpoints/{id}

Deshabilita el endpoint (baja lógica: deja de recibir entregas nuevas). No es un borrado físico — el ledger y el historial son append-only por diseño, y esta fila sigue existiendo con active: false.

Authorizationrequerido

Errores posibles: webhook_endpoint.not_found.

200
{ "id": "c1a9f2e0-...", "object": "webhook_endpoint", "deleted": true }

Checkout público (/public)

Diseñados para llamarse directo desde el navegador del pagador (así es como los usa winal.js): sin Authorization, el client_secret del intent autentica. La proyección de respuesta es reducida — nunca incluye client_secret, attempts, provider_ref ni metadata, y tampoco livemode (a diferencia de la respuesta autenticada de /v1).

POST/public/payment_intents/{id}/confirm

Cuerpo

client_secretstring, requerido
payment_tokenstring, requerido
payment_methodstring, requerido
tip_minorint64, opcional, no negativo — igual semántica que en /v1/payment_intents/{id}/confirm (reemplaza la propina existente si se manda).

Si el id no existe o el client_secret no coincide, la respuesta es siempre el mismo 404 genérico — nunca revela cuál de los dos falló (defensa contra fuerza bruta).

request
POST /public/payment_intents/5b6b8b3e-.../confirm
Content-Type: application/json

{
  "client_secret": "pi_secret_9fZ3kQ7bV1x...",
  "payment_token": "tok_sim_ok",
  "payment_method": "card",
  "tip_minor": 3000
}
200 · proyección pública
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 3000,
  "total_minor": 87900,
  "currency": "MXN",
  "status": "processing"
}
GET/public/payment_intents/{id}

Query

client_secretrequerido

Usado por el polling interno de winal.js cada 2 s hasta un estado terminal.

request
GET /public/payment_intents/5b6b8b3e-...
X-Winal-Client-Secret: pi_secret_9fZ3kQ7bV1x...
200 · con next_action (SPEI)
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 0,
  "total_minor": 84900,
  "currency": "MXN",
  "status": "requires_action",
  "next_action": {
    "type": "bank_transfer",
    "clabe": "646180473921058317",
    "beneficiary": "SIM SPEI",
    "expires_at": "2026-07-06T18:30:00Z"
  }
}

Eventos

Detalle narrativo, patrón de polling y el shape completo del envelope en Eventos y polling.

GET/v1/events

Stream de polling sobre las entregas de webhook del tenant — alternativa a recibir HTTP entrante, pensado para desarrollo local y para el CLI oficial winal listen. Solo devuelve filas si el tenant tiene al menos un webhook_endpoint activo registrado.

Authorizationrequerido

Query

after_idint64, opcional (default 0) — cursor exclusivo: devuelve id > after_id.
limitint, opcional (default 50, tope 200; valores mayores se recortan).
request
GET /v1/events?after_id=0&limit=50
Authorization: Bearer sk_test_...
200 · respuesta real
{
  "object": "list",
  "data": [
    {
      "id": 15,
      "object": "event",
      "event_id": "5e332767-ae86-42e3-b395-cc325de90f2b",
      "event_type": "payment_intent.succeeded",
      "delivered": false,
      "created_at": "2026-07-07T22:55:49Z",
      "data": {
        "event_id": "5e332767-ae86-42e3-b395-cc325de90f2b",
        "event_type": "payment_intent.succeeded",
        "created_at": "2026-07-07T22:55:48Z",
        "api_version": "2026-07-01",
        "livemode": false,
        "data": {
          "id": "f5f3fc01-8d3b-4abd-881e-52664a1d9c44",
          "object": "payment_intent",
          "status": "succeeded",
          "amount_minor": 10000,
          "tip_minor": 1500,
          "total_minor": 11500,
          "currency": "MXN",
          "metadata": { "payment_link_id": "pl_smoke_fosos" }
        }
      }
    }
  ]
}

Facturas (CFDI)

Guía narrativa completa (PUE vs. PPD, complemento de pagos) en Facturación CFDI.

POST/v1/invoices

Timbra un CFDI de ingreso, método de pago PUE, sobre un payment_intent ya succeeded.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

payment_intent_iduuid, requerido
receptorobjeto requerido: rfc, nombre, uso_cfdi, regimen_fiscal, cp (todos requeridos)

Errores posibles: invoice.invalid_body, invoice.invalid_receptor, invoice.pac_error (ver Errores).

request
POST /v1/invoices
Authorization: Bearer sk_test_...
Idempotency-Key: 1a2b3c4d-...
Content-Type: application/json

{
  "payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
  "receptor": {
    "rfc": "XAXX010101000",
    "nombre": "Publico en general",
    "uso_cfdi": "S01",
    "regimen_fiscal": "616",
    "cp": "06600"
  }
}
201 · CFDI timbrado
{
  "id": "...",
  "object": "invoice",
  "status": "stamped",
  "payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
  "uuid_fiscal": "...",
  "serie": "A", "folio": "1",
  "total_minor": 55000, "base_minor": 47414, "iva_minor": 7586,
  "currency": "MXN",
  "receptor": { "...": "..." },
  "pac": "facturama",
  "metodo_pago": "PUE",
  "parcialidades": 0,
  "created_at": "...", "updated_at": "..."
}
POST/v1/invoices/ppd

Emite un CFDI método de pago PPD por un total acordado, SIN cobro previo — se liquidará después con uno o más REP.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

total_minorint64, requerido, > 0 (incluye IVA)
currencystring ISO 4217, opcional (default MXN)
receptorobjeto requerido, mismos 5 campos que en POST /v1/invoices
descripcionstring, opcional

Errores posibles: invoice.invalid_body, invoice.invalid_receptor, invoice.invalid_currency, invoice.pac_error.

request
POST /v1/invoices/ppd
Authorization: Bearer sk_test_...
Idempotency-Key: 2b3c4d5e-...
Content-Type: application/json

{
  "total_minor": 348000,
  "currency": "MXN",
  "receptor": {
    "rfc": "XAXX010101000",
    "nombre": "Publico en General",
    "uso_cfdi": "S01",
    "regimen_fiscal": "616",
    "cp": "06600"
  },
  "descripcion": "Servicios profesionales - anticipo PPD"
}
201 · respuesta real
{
  "id": "5cee05bd-de0c-4961-98eb-e0cacfc6aae8",
  "object": "invoice",
  "status": "stamped",
  "total_minor": 348000,
  "base_minor": 300000,
  "iva_minor": 48000,
  "currency": "MXN",
  "metodo_pago": "PPD",
  "saldo_insoluto_minor": 348000,
  "parcialidades": 0,
  "pac": "facturama",
  "created_at": "2026-07-07T23:02:30Z",
  "updated_at": "2026-07-07T23:02:31Z"
}
POST/v1/invoices/{id}/payments

Registra un pago sobre una factura PPD ya stamped y timbra su complemento de pagos 2.0 (REP).

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

payment_intent_iduuid, requerido — cobro ya succeeded que liquida (parte de) el saldo insoluto

Errores posibles: invoice.invalid_body, invoice.not_found, invoice.not_stamped, invoice.pac_error.

request
POST /v1/invoices/5cee05bd-.../payments
Authorization: Bearer sk_test_...
Idempotency-Key: 3c4d5e6f-...
Content-Type: application/json

{ "payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52" }
201 · shape esperado
{
  "id": "...",
  "object": "invoice_payment",
  "invoice_id": "5cee05bd-de0c-4961-98eb-e0cacfc6aae8",
  "payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
  "parcialidad": 1,
  "monto_minor": 55000,
  "saldo_anterior_minor": 348000,
  "saldo_insoluto_minor": 293000,
  "currency": "MXN",
  "status": "stamped",
  "rep_uuid": "...",
  "created_at": "...", "updated_at": "..."
}
GET/v1/invoices/{id}/payments

Lista los REP timbrados contra una factura PPD.

Authorizationrequerido
200 · respuesta real (factura sin pagos aún)
{ "object": "list", "data": [] }

Cobranza (Receivables)

Guía narrativa completa (Payment Link automático, marcado como paid, auto-CFDI) en Cobranza.

POST/v1/receivables

Crea una cuenta por cobrar; internamente crea un Payment Link de un solo uso.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

customer_name / customer_emailstring, requeridos
customer_phonestring, opcional — requerido solo para whatsapp_link
customer_rfc / customer_uso_cfdi / customer_regimen_fiscal / customer_cpopcionales, pero van juntos o ninguno (auto-CFDI al pagarse)
conceptostring, requerido
amount_minorint64, requerido, > 0
currencystring ISO 4217, requerido
due_datedatetime ISO-8601, requerido

Errores posibles: receivable.missing_fields, receivable.invalid_amount, receivable.invalid_currency, receivable.invalid_date, receivable.incomplete_fiscal_receptor.

201 · respuesta real
{
  "id": "0ec348fb-2341-4f31-b870-9cc4d3087b29",
  "object": "receivable",
  "customer_name": "María López",
  "customer_email": "maria.lopez@example.mx",
  "customer_phone": "5215512345678",
  "amount_minor": 150000,
  "currency": "MXN",
  "concepto": "Mensualidad julio 2026 - plan Pro",
  "due_date": "2026-07-20T00:00:00+00:00",
  "status": "open",
  "payment_link_id": "27011835-fd92-44dd-8d6c-549c00536703",
  "payment_link_url": "/pay/QOECDPCH2HC",
  "created_at": "2026-07-08T02:19:53.743499+00:00"
}
GET/v1/receivables · GET/v1/receivables/{id}

Lista o lee una cuenta por cobrar. status es derivado: open | paid | overdue | canceled.

Authorizationrequerido

Errores posibles: receivable.not_found.

GET/v1/receivables/{id}/whatsapp_link

Arma la URL https://wa.me/... con el mensaje de cobro pre-redactado.

Authorizationrequerido

Errores posibles: receivable.no_phone (sin customer_phone capturado).

200 · respuesta real
{ "url": "https://wa.me/5215512345678?text=Hola%20Mar%C3%ADa..." }
GET/v1/receivables/statement?customer_email=

Estado de cuenta agregado de un cliente: sus cuentas y los totales abierto/vencido/pagado.

Authorizationrequerido
200 · respuesta real
{
  "customer_email": "maria.lopez@example.mx",
  "receivables": [ { "...": "..." } ],
  "total_open_minor": 150000,
  "total_overdue_minor": 0,
  "total_paid_minor": 0
}
GET/v1/reports/aging

Antigüedad de saldos por cliente, en 4 cubos contables estándar.

Authorizationrequerido
200 · respuesta real (nombres de campo exactos)
{
  "object": "list",
  "data": [
    {
      "customer_email": "aging@prueba.mx",
      "customer_name": "Prueba Aging",
      "currency": "MXN",
      "bucket0_to30_minor": 30000,
      "bucket31_to60_minor": 0,
      "bucket61_to90_minor": 0,
      "bucket90_plus_minor": 0,
      "total_minor": 30000
    }
  ]
}

Conciliación bancaria

Guía narrativa completa (presets, mapeo de columnas, tipos de excepción) en Conciliación bancaria.

POST/v1/reconciliation/bank-statements

Importa el CSV del estado de cuenta bancario. Multipart (file) o cuerpo crudo. Sin Idempotency-Key — su idempotencia real es el hash del archivo.

Authorizationrequerido

Campos (multipart o query)

bankstring, requerido
period_start / period_endyyyy-MM-dd, requeridos
tolerance_daysint, opcional
presetbbva | banorte | santander, o usa el mapeo explícito
date_column / description_column / credit_column / debit_columnint (0-based), requeridos si no hay preset
reference_columnint, opcional
has_headerbool, opcional (default true)

Errores posibles: ver la tabla completa en Conciliación bancaria.

201 · respuesta real
{
  "id": "271a21c8-be04-4d6e-909a-8f1394fd333a",
  "object": "bank_statement",
  "bank": "bbva",
  "period_start": "2026-07-01",
  "period_end": "2026-07-08",
  "filename": "estado_bbva.csv",
  "lines_total": 2,
  "matched": 0,
  "partial": 0,
  "unmatched": 1,
  "exceptions": 2,
  "already_imported": false
}
GET/v1/reconciliation/bank-statements · GET/v1/reconciliation/bank-statements/{id}/lines?match_status=

Lista los estados de cuenta importados, o las líneas de uno (filtro opcional matched/unmatched/partial).

Authorizationrequerido

Errores posibles: bank_statement.invalid_match_status.

200 · línea real
{
  "id": 3,
  "object": "bank_statement_line",
  "value_date": "2026-07-01",
  "description": "SPEI RECIBIDO ANTECH",
  "reference": "REF001",
  "credit_minor": 50000,
  "currency": "MXN",
  "match_status": "unmatched"
}

Autofactura pública

Guía narrativa completa (receipt_code, anti-enumeración) en Facturación CFDI → Autofactura.

POST/public/autofactura/{slug}/invoices

Sin Authorization — la llave es receipt_code + rfc.

Cuerpo

receipt_codestring, requerido — formato W-XXXXX, viene en cada payment_intent
rfcstring, requerido — formato SAT
nombre / uso_cfdi / regimen_fiscal / cpstring, requeridos
emailstring, opcional (captura sin efecto en el timbrado hoy)

Errores posibles: autofactura.invalid_body, autofactura.invalid_rfc, autofactura.not_available (404), autofactura.receipt_not_found (404), más los errores de invoice.* del PAC reenviados tal cual.

400 · respuesta real (sin PAC configurado, este servidor)
{
  "error": {
    "type": "invalid_request_error",
    "code": "invoice.pac_error",
    "message": "El PAC rechazó el timbrado (CFDI 492400bb-... quedó en 'error'): ",
    "doc_url": "https://winal.com.mx/docs/errores.html#err-invoice.pac_error",
    "request_id": "0HNMSIOBNVS9K:00000001"
  }
}
GET/public/autofactura/{slug}/invoices/{id}/xml?receipt_code=&rfc= · GET.../pdf?receipt_code=&rfc=

Descarga del CFDI ya timbrado; ambos parámetros deben coincidir con el CFDI.

Errores posibles: invoice.not_stamped, autofactura.receipt_not_found.

Reportes

Guía narrativa con el significado de cada campo en Reportes.

GET/v1/reports/savings

Cuánto le ahorró/recuperó Winal al tenant en el período: ruteo consciente de costo, retry cross-conector y dunning de suscripciones.

Authorizationrequerido

Query

from / toISO-8601, opcionales (default: últimos 30 días)

Errores posibles: reports.invalid_period.

200 · respuesta real
{
  "object": "reporting.savings",
  "routing_saved_minor": 0,
  "retry_recovered_minor": 0,
  "dunning_recovered_minor": 0,
  "total_minor": 0,
  "by_connector": [],
  "period_from": "2026-06-07T23:02:03Z",
  "period_to": "2026-07-07T23:02:03Z"
}
GET/v1/reports/polizas

Pólizas contables del período, formato CONTPAQi o Aspel-COI, listas para importar.

Authorizationrequerido

Query

from / toISO-8601, opcionales (default: últimos 30 días)
formatrequerido, sin default: contpaqi | aspel_coi

Errores posibles: reports.invalid_period, reports.invalid_format.

request
GET /v1/reports/polizas?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&format=contpaqi
Authorization: Bearer sk_test_...
200 · text/plain, fragmento real
P  20260706    3         1 1 0          Poliza diario Winal 2026-07-06
M  108-001                        1          0 150.00               0          0.00   charge: charge:attempt_852d...
GET/v1/reports/cash-cut

Corte de caja del turno: desglose por método/conector (cobrado, propinas, operaciones), totales y devoluciones. Ver Reportes → Multisucursal para el detalle narrativo.

Authorizationrequerido

Query

from / toISO-8601, ambos requeridos (sin default: no hay "turno" sin rango explícito)
branch / registeropcionales, igualdad exacta contra metadata.branch/metadata.register del intent — no validan contra ningún catálogo.

Errores posibles: reports.invalid_period, reports.invalid_currency.

200 · respuesta real, sin filtrar por branch (trae by_branch)
{
  "object": "reporting.cash_cut",
  "currency": "MXN",
  "period_from": "2026-07-01T00:00:00Z",
  "period_to": "2026-07-08T00:00:00Z",
  "by_method": [
    { "key": "card", "charged_minor": 256490, "tip_minor": 6500, "total_minor": 262990, "operation_count": 13 }
  ],
  "by_connector": [
    { "key": "sim", "charged_minor": 281490, "tip_minor": 6500, "total_minor": 287990, "operation_count": 14 }
  ],
  "totals": { "charged_minor": 281490, "tip_minor": 6500, "total_minor": 287990, "operation_count": 14 },
  "refunds": { "count": 0, "amount_minor": 0 },
  "by_branch": [
    { "key": "(sin sucursal)", "charged_minor": 1589124, "tip_minor": 9500, "total_minor": 1598624, "operation_count": 27 },
    { "key": "centro", "charged_minor": 800, "tip_minor": 0, "total_minor": 800, "operation_count": 1 }
  ]
}

Onboarding de sub-comercios

Guía narrativa completa (flujo draft → submit → aprobado, campos y documentos) en Onboarding. Requisito para Winal Connect.

POST/v1/onboarding/applications

Alta mínima de una solicitud, en draft.

Authorizationrequerido

Cuerpo

legal_namestring, requerido
person_type"fisica" | "moral", requerido
contact_emailstring, requerido
resto de campos (ver Onboarding)opcionales al crear; completos exige submit

Errores posibles: onboarding_application.invalid_legal_name, invalid_person_type, invalid_contact_email.

201 · respuesta real
{
  "id": "75deacfb-ff0d-476e-8292-f9471941694a",
  "object": "onboarding_application",
  "status": "draft",
  "legal_name": "Tienda Docs SA de CV",
  "person_type": "moral",
  "contact_email": "docs@example.mx",
  "created_at": "2026-07-08T06:48:56.880311+00:00",
  "updated_at": "2026-07-08T06:48:56.880311+00:00",
  "documents": []
}
PUT/v1/onboarding/applications/{id}

Patch parcial (campos null/omitidos no se tocan), solo mientras la solicitud sea editable (draft/needs_info). Mismo cuerpo que POST, más documents[] (ver Onboarding para los 4 tipos requeridos).

Authorizationrequerido

Errores posibles: onboarding_application.not_found, onboarding_application.transition_conflict, onboarding_application.invalid_document.

200 · respuesta real (fragmento)
{
  "id": "75deacfb-...", "object": "onboarding_application", "status": "draft",
  "rfc": "TDS900101AB1", "clabe_masked": "**** **** **** 0004",
  "documents": [ { "document_type": "ine", "reference": "INE-DOC-001", "reference_hash": "a1b2c3…", "created_at": "..." }, "..." ]
}
POST/v1/onboarding/applications/{id}/submit

Dispara la verificación (síncrona en fase 0): completitud → formato de RFC/CLABE → check de listas (simulado). Resuelve a approved, needs_info o rejected en la misma llamada.

Authorizationrequerido

Errores posibles: onboarding_application.missing_fields, missing_documents, transition_conflict.

200 · respuesta real (aprobada)
{
  "id": "75deacfb-...", "object": "onboarding_application", "status": "approved",
  "status_reason": "Verificación automática aprobada: RFC y CLABE con formato válido, sin coincidencias en listas.",
  "resolved_by": "system:auto_verification",
  "submitted_at": "2026-07-08T06:49:17.36Z", "resolved_at": "2026-07-08T06:49:17.36Z"
}
GET/v1/onboarding/applications · GET/v1/onboarding/applications/{id}

Lista (?status=, ?limit= opcionales; sin documents) o lee una solicitud por id (con documents[]).

Authorizationrequerido

Errores posibles: onboarding_application.not_found.

La resolución manual (aprobar/rechazar/pedir información) es una operación de portal → Onboarding para operadores de Winal, montada bajo /admin/tenants/{tenantId}/onboarding/applications/... — no se documenta aquí como endpoint de tu integración.

Winal Connect

Guía narrativa completa (el modelo, las dos formas de marcar un split, la dispersión) en Winal Connect.

POST/v1/connect/accounts

Liga un sub-comercio con una solicitud de Onboarding ya approved.

Authorizationrequerido

Cuerpo

onboarding_application_idstring (uuid), requerido
connector_keystring, opcional (default "stp"; usa "sim" en pruebas)

Errores posibles: connect.invalid_application_id, connect.application_not_found, connect.application_not_approved, connect.account_conflict.

201 · respuesta real
{
  "id": "0d0de738-0133-4625-97e2-9c285ae20f9e",
  "object": "connect_account",
  "onboarding_application_id": "0732573a-5567-4b2a-87b6-8721654c5060",
  "settlement_clabe_masked": "**** **** **** 0004",
  "sub_merchant_name": "Sub Comercio Sim SA de CV",
  "status": "active",
  "connector_key": "sim",
  "livemode": false,
  "created_at": "2026-07-08T06:49:58.292982+00:00"
}
GET/v1/connect/accounts · GET/v1/connect/accounts/{id}

Lista (?limit=) o lee una cuenta Connect. La CLABE del sub nunca se expone completa.

Authorizationrequerido

Errores posibles: connect.account_not_found.

POST/v1/connect/transfers

Split manual post-cobro (multi sub-comercio). application_fee_minor + Σ splits[].amount_minor debe ser exactamente charge_amount_minor.

Authorizationrequerido
Idempotency-Keyrequerido (UUID) — compromete dinero

Cuerpo

payment_intent_idstring (uuid), requerido
charge_amount_minorint64, requerido, > 0
application_fee_minorint64, requerido (puede ser 0)
currencyISO 4217, requerido
splits[{connect_account_id, amount_minor}], al menos uno

Errores posibles: connect.invalid_charge, connect.missing_allocations, connect.invalid_allocation, connect.split_mismatch, connect.account_not_found.

201 · respuesta real
{
  "id": "53fb6fd5-92c8-4e13-93a1-f01aefb21ce8",
  "object": "connect_transfer",
  "payment_intent_id": "aaab3de3-3cfe-4d55-9bbd-24a89977abf7",
  "charge_amount_minor": 100000,
  "application_fee_minor": 10000,
  "currency": "MXN",
  "status": "split",
  "created_at": "2026-07-08T06:50:09.078051+00:00",
  "splits": [
    { "id": "09702eba-...", "connect_account_id": "0d0de738-...", "amount_minor": 90000, "status": "dispersing", "payout_id": "fa888dfc-..." }
  ]
}
GET/v1/connect/transfers · GET/v1/connect/transfers/{id}

Lista (?limit=) o lee un transfer, con sus splits[].

Authorizationrequerido

Errores posibles: connect.transfer_not_found.

Payouts

Guía narrativa completa (la máquina de estados, STP vs. Sim) en Payouts.

POST/v1/payouts

Ordena una dispersión SPEI a una CLABE. Sin custodia (ADR-0001): sale de la cuenta del propio comercio.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

clabestring, 18 dígitos con dígito de control válido, requerido
beneficiary_namestring, requerido
beneficiary_rfcstring, opcional
amount_minorint64, requerido, > 0
currencyISO 4217, requerido (solo MXN)
conceptostring, requerido
referencestring, opcional (se genera si falta)
connector_keyopcional (default "stp"; usa "sim" en pruebas)

Errores posibles: payout.missing_fields, payout.invalid_amount, payout.invalid_clabe, payout.unsupported_currency.

201 · respuesta real
{
  "id": "b4e5115a-e401-48b6-9b4d-e80b7a915748",
  "object": "payout",
  "clabe": "646180157000000004",
  "beneficiary_name": "Proveedor Docs SA de CV",
  "amount_minor": 250000,
  "currency": "MXN",
  "concepto": "pago de prueba docs",
  "reference": "2441786",
  "status": "processing",
  "connector_key": "sim",
  "livemode": false,
  "created_at": "2026-07-08T06:46:43.157657+00:00"
}
GET/v1/payouts · GET/v1/payouts/{id}

Lista (?limit=, default 100, máx. 500) o lee un payout — provider_ref/tracking_key/failure_reason aparecen cuando el Worker ya ejecutó la orden.

Authorizationrequerido

Errores posibles: payout.not_found.

Billers (pago de servicios)

Guía narrativa completa en Pago de servicios.

GET/v1/billers

Catálogo global (sin variación por tenant). ?category= opcional.

Authorizationrequerido
200 · respuesta real (fragmento)
{ "object": "list", "data": [
  { "object": "biller", "code": "cfe", "name": "CFE (Comisión Federal de Electricidad)",
    "category": "luz", "reference_label": "Número de servicio (10 a 12 dígitos)", "active": true },
  "... (agua_cdmx, telcel_recarga, telmex, izzi)"
] }
POST/v1/billers/{code}/inquiry

Consulta de adeudo — NO mueve dinero, no exige Idempotency-Key.

Authorizationrequerido

Cuerpo: { "reference": "string, requerido" }

Errores posibles: biller.missing_reference, biller.not_found, biller.invalid_reference, biller.reference_not_found.

200 · respuesta real
{
  "object": "biller_inquiry", "biller_code": "cfe", "biller_name": "CFE (Comisión Federal de Electricidad)",
  "reference": "1234567890", "amount_due_minor": 118700, "currency": "MXN",
  "service_holder_name": "Cliente simulado (ref. 1234567890)", "due_date": "2026-07-18T06:47:41.85Z"
}
POST/v1/service-payments

Confirma el pago del servicio ante el biller.

Authorizationrequerido
Idempotency-Keyrequerido (UUID) — validado por este endpoint mismo

Cuerpo

biller_codestring, requerido
referencestring, requerido
amount_minorint64, opcional — si viene, debe igualar el adeudo vigente
payment_intent_iduuid, opcional — solo correlación, sin FK real

Errores posibles: service_payment.missing_fields, service_payment.amount_mismatch, service_payment.idempotency_conflict.

201 · respuesta real (pagado)
{
  "id": "3f490a8e-630e-4a4b-ba4e-81be735c0c14", "object": "service_payment",
  "biller_code": "cfe", "reference": "1234567890", "amount_minor": 118700, "currency": "MXN",
  "status": "paid", "provider_ref": "SIMBILL-7188545cf02c4532b9681371fbe806b3",
  "created_at": "2026-07-08T06:47:46.86Z", "updated_at": "2026-07-08T06:47:46.86Z"
}
201 · el biller rechaza la confirmación (no es error HTTP)
{ "id": "...", "object": "service_payment", "status": "failed",
  "failure_reason": "El biller 'Telmex' rechazó la confirmación del pago para la referencia '...' (simulado)." }
GET/v1/service-payments · GET/v1/service-payments/{id}

Lista (?limit=) o lee un pago de servicio.

Authorizationrequerido

Errores posibles: service_payment.not_found.

Terminales (card-present)

El alta y administración de terminales SmartPOS (número de serie, modelo, sucursal, estado active/inactive/lost) es una operación de portal → Terminales, no un endpoint público de /v1 — tu integración solo necesita el id de la terminal ya activa, para mandarlo como metadata.terminal_id al cobrar card_present o CoDi en mostrador. Ver Métodos de pago → Card-present y CoDi en mostrador.