Cobranza

MODO PRUEBA

Una cuenta por cobrar (receivable) es un adeudo con fecha de vencimiento que Winal convierte automáticamente en un Payment Link de un solo uso: crea la cuenta, mándale al cliente el link (o el link de WhatsApp que Winal arma por ti), y cuando la pague, la cuenta se marca paid sola — no hay que conciliar nada a mano.

Crea una cuenta por cobrar

concepto es el campo obligatorio que describe el adeudo (aparece en el link de pago y en el mensaje de WhatsApp). Los cuatro datos fiscales del receptor (customer_rfc, customer_uso_cfdi, customer_regimen_fiscal, customer_cp) son opcionales, pero van juntos o ninguno: si los das, Winal intenta timbrar el CFDI automáticamente en cuanto la cuenta se paga.

bash
curl -s https://api.winal.com.mx/v1/receivables \
  -H "Authorization: Bearer $SK" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "María López",
    "customer_email": "maria.lopez@example.mx",
    "customer_phone": "5215512345678",
    "concepto": "Mensualidad julio 2026 - plan Pro",
    "amount_minor": 150000,
    "currency": "MXN",
    "due_date": "2026-07-20T00:00:00Z"
  }'
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"
}

payment_link_url es una ruta relativa — antepón el host de tu Winal (https://winal.com.mx{payment_link_url}) para armar la URL completa que le mandas al cliente. status es derivado: openpaid (al cobrarse) o overdue (pasado due_date sin pagar); también puede quedar canceled.

El link de WhatsApp, listo para mandar

bash
curl -s https://api.winal.com.mx/v1/receivables/0ec348fb-.../whatsapp_link \
  -H "Authorization: Bearer $SK"
200 · respuesta real
{
  "url": "https://wa.me/5215512345678?text=Hola%20Mar%C3%ADa%20L%C3%B3pez%2C%20tienes%20un%20adeudo%20de%201500.00%20MXN%20por%20concepto%20de%20%22Mensualidad%20julio%202026%20-%20plan%20Pro%22%20con%20vencimiento%2020%2F07%2F2026.%20Puedes%20pagarlo%20aqu%C3%AD%3A%20%2Fpay%2FQOECDPCH2HC"
}

Es una URL https://wa.me/... lista para abrir directo (botón, link en un correo, etc.) — el mensaje ya trae el monto en pesos, el concepto y la ruta del link de pago pre-armados. Requiere que la cuenta tenga customer_phone; si no, 400 receivable.no_phone.

El pago marca la cuenta sola

Cuando el cliente paga el link (POST /public/payment_links/{slug}/intents + confirm — ver Referencia de API), el intent nace con metadata.payment_link_id apuntando al link de la cuenta. Al llegar payment_intent.succeeded, un handler del Worker (el mismo tópico que despacha webhooks) revisa esa metadata: si pertenece a un receivable, lo marca paid — de forma idempotente, nunca dos veces por reentregas del evento — y, si la cuenta trae los cuatro datos fiscales completos, dispara el timbrado automático del CFDI.

200 · el mismo receivable, tras pagarse el link
{
  "id": "0ec348fb-2341-4f31-b870-9cc4d3087b29",
  "object": "receivable",
  "customer_name": "María López",
  "...": "...",
  "status": "paid",
  "paid_at": "2026-07-08T02:21:05.20706+00:00",
  "paid_payment_intent_id": "c252d2af-18b3-4fd3-8296-81d2c560db02",
  "created_at": "2026-07-08T02:19:53.743499+00:00"
}
El error del auto-CFDI nunca se oculta
Si el timbrado automático falla (p. ej. sin PAC configurado — ver Facturación CFDI), la cuenta queda paid igual (el cobro sí sucedió) pero con cfdi_error visible en la respuesta — nunca se reintenta solo ni se esconde el error.

No hay que hacer nada para que esto ocurra: es automático en cuanto confirmas el pago del link, sea desde el checkout hosteado o desde tu propia integración.

Recordatorios automáticos

Un scheduler del Worker revisa cada 15 minutos qué cuentas tienen un recordatorio pendiente (offsets configurables en el portal, p. ej. antes y después del vencimiento) y manda un correo por el SMTP que configures en portal → Cobranza → Recordatorios — reentrante e idempotente: un ciclo de más, o uno perdido por un reinicio, nunca duplica ni pierde un recordatorio. El link de WhatsApp de arriba es la vía manual complementaria cuando prefieres mandarlo tú mismo.

Aging: antigüedad de saldos

GET /v1/reports/aging agrupa por cliente el saldo vencido en los cuatro cubos contables estándar (0-30, 31-60, 61-90, 90+ días), para priorizar cobranza.

bash
curl -s https://api.winal.com.mx/v1/reports/aging -H "Authorization: Bearer $SK"
200 · respuesta real
{
  "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
    }
  ]
}

Nota los nombres reales de los campos del bucket — no llevan guión bajo entre el número y la palabra to (bucket0_to30_minor, no bucket_0_to_30_minor).

Estado de cuenta de un cliente

bash
curl -s "https://api.winal.com.mx/v1/receivables/statement?customer_email=maria.lopez@example.mx" \
  -H "Authorization: Bearer $SK"
200 · respuesta real
{
  "customer_email": "maria.lopez@example.mx",
  "receivables": [ { "...": "mismo shape del receivable" } ],
  "total_open_minor": 150000,
  "total_overdue_minor": 0,
  "total_paid_minor": 0
}

Endpoints

MétodoRutaNotas
POST/v1/receivablesCrea la cuenta y, por dentro, un Payment Link de un solo uso.
GET/v1/receivablesLista las cuentas del tenant.
GET/v1/receivables/{id}404 receivable.not_found si no existe.
GET/v1/receivables/{id}/whatsapp_linkRequiere customer_phone capturado.
GET/v1/receivables/statement?customer_email=Estado de cuenta agregado de un cliente.
GET/v1/reports/agingAntigüedad de saldos por cliente, en 4 cubos.

Errores de receivables

HTTPcodeCausa
400receivable.missing_fieldsFalta customer_name, customer_email o concepto.
400receivable.invalid_amountamount_minor no es positivo.
400receivable.invalid_currencycurrency no es ISO 4217 válida.
400receivable.invalid_datedue_date inválida.
400receivable.incomplete_fiscal_receptorSe mandó solo alguno de los cuatro datos fiscales — van juntos o ninguno.
404receivable.not_foundEl id no existe.
400receivable.no_phoneSe pidió whatsapp_link sin customer_phone capturado.

Ver el envelope completo de error en Errores.