Saltar al contenido

Documentación API

Martina F.
API de Lucky Cobros · versión 1.0

Cobrá con CVU, conciliá solo y pagá retiros desde tu sistema

Con la API, tu plataforma le asigna un CVU a cada depósito de tus jugadores, consulta saldos y movimientos y paga retiros a cualquier CBU o CVU. Cada retiro te llega con un aviso firmado a tu sistema.

URL base https://api.luckycobros.com.ar/api/v1

Es la misma URL que ves en Integración del panel. Todos los endpoints de la API pública cuelgan de /external.

Qué está disponible en la versión 1.0

Ya podés pedir CVU, crear depósitos esperados, consultar saldos y movimientos, pedir retiros y configurar tu webhook. La acreditación automática de cobros y sus avisos transaction.* están en Próximamente: hasta que te confirmemos que están activos en tu cuenta, los depósitos esperados se crean, pero ningún cobro se acredita ni te avisa. El contrato de esos avisos ya es el definitivo, así que podés dejar tu receptor listo.

  1. 1
    Pedís un CVUTu sistema crea un depósito esperado con el monto y el CUIT del jugador.
  2. 2
    El jugador transfiereDesde cualquier banco o app de pagos, a la cuenta CVU que le mostrás.
  3. 3
    Se concilia solo PróximamenteEl cobro se acredita a tu saldo y se cruza con el depósito esperado por CUIT.
  4. 4
    Te avisamos PróximamenteTu sistema recibe un webhook firmado: acreditado, o retenido para revisión si algo no coincide.

Formato general

  • JSON en UTF-8 en pedidos y respuestas. Mandá Content-Type: application/json en los POST y PUT.
  • Montos en pesos argentinos como número con punto decimal: 25000 o 1500000.5. En el listado de transacciones los montos llegan como texto ("25000"): convertilos con un tipo decimal, no con punto flotante.
  • Fechas en ISO 8601, en UTC: 2026-10-28T17:03:11.482Z es el 28/10/2026 a las 14:03 en Argentina.
  • CUIT o CUIL con 11 dígitos y sin guiones (27402187354). CBU o CVU con 22 dígitos.
  • Códigos de respuesta: los GET y el PUT responden 200; los POST que se procesan responden 201. En un retiro, el resultado real está en el campo status del cuerpo.

Descargas

Autenticación

Cada pedido lleva tu API key en el encabezado X-API-Key.

Te damos la key en el alta; cuando tengas el panel, también vas a poder crearla en Integración › API keys. Tiene 64 caracteres y se entrega una sola vez: guardala en tu gestor de secretos. Nunca la pongas en el front de tu sitio ni en una app: solo tu servidor tiene que conocerla.

Encabezado de autenticación
curl "https://api.luckycobros.com.ar/api/v1/external/balance" \
  -H "X-API-Key: $LUCKY_API_KEY"

Permisos

Cada key tiene uno o dos permisos: read (consultar) y write (operar). Cada endpoint exige uno de los dos.

Permisos por endpoint
PermisoEndpointsPara qué
readGET /external/balance · /cbu · /transactions · /webhookConsultar saldos, pedir un CVU, listar movimientos y leer la configuración del webhook.
writePOST /external/deposit/create · /transfer · /transfer/verify · PUT /external/webhookCrear depósitos esperados, pedir y validar retiros, configurar el webhook.

write no incluye read

Una key que solo tiene write recibe 403 permisos insuficientes en los GET. Si tu integración consulta y opera, la key necesita los dos permisos. Una buena práctica es usar dos keys: una de solo lectura para reportes y otra de lectura y escritura para la operación.

IPs permitidas

Cada key puede tener una lista de IPs públicas desde las que se acepta. Si la lista está vacía, la key funciona desde cualquier IP. Es una capa más de protección: no reemplaza mantener la key en secreto.

  • Cargá cada IP exacta (por ejemplo 203.0.113.24). Hoy no se aceptan rangos ni notación CIDR.
  • La IP se controla antes que el permiso: un pedido desde una IP no habilitada recibe 403 con "code": "IP_NOT_WHITELISTED", sin importar el endpoint.

Rotar y revocar

Las keys no vencen. Para cambiar una: pedinos la nueva a soporte, desplegala en tus servidores y recién después pedí que revoquemos la anterior (cuando tengas el panel, lo hacés vos en Integración › API keys). Una key revocada deja de funcionar al instante.

Respuestas de autenticación

Estos errores tienen un cuerpo propio, con message y code:

Errores de autenticación
HTTPCuerpo exactoCuándo
401{"message":"API Key faltante","code":null}Falta el encabezado X-API-Key.
401{"message":"API Key inválida o revocada","code":null}La key no existe o fue revocada.
403{"message":"IP no whitelisted","code":"IP_NOT_WHITELISTED"}La IP del pedido no está en la lista de la key.
403{"message":"permisos insuficientes","code":null}La key no tiene el permiso del endpoint.

Entornos

Hoy hay un único entorno: producción.

Entornos
EntornoURL baseEstadoPara qué
Producciónhttps://api.luckycobros.com.ar/api/v1DisponibleOperación real: los cobros y retiros mueven plata de verdad.
Pruebas—PróximamenteUn entorno aislado con plata ficticia para probar de punta a punta.

Cómo probar mientras tanto

  • Usá una key aparte para tu entorno de staging (por ejemplo, "Integración · pruebas"), con las IPs de esos servidores. Así la revocás sin tocar producción.
  • Los GET no mueven plata: balance, CVU, transacciones y webhook se pueden llamar sin riesgo.
  • Los avisos se prueban en tu máquina, firmados con tu secreto y sin plata real: seguí la guía Probar webhooks localmente.
  • Para un retiro de prueba, mandá $ 1,00 a una cuenta propia externa (por ejemplo, la cuenta bancaria de tu empresa, no una de tus cuentas CVU de cobro) y validá el circuito completo: respuesta, aviso y movimiento en el listado.

Estado del servicio

GET /health responde {"status":"ok"} sin API key. Usalo en tu monitoreo: ver referencia.

Errores

Decidí por el código HTTP; el texto de message es para tus logs.

Los errores llegan en JSON con dos formas. Los de autenticación traen message y code; el resto, statusCode, message y error:

403Autenticación (401 / 403)
{
  "message": "IP no whitelisted",
  "code": "IP_NOT_WHITELISTED"
}
400Resto de los errores
{
  "statusCode": 400,
  "message": "saldo insuficiente para el retiro",
  "error": "Bad Request"
}
Códigos HTTP
HTTPSignificadoQué hacer
200OKConsulta (GET) o configuración (PUT) correcta.
201ProcesadoEl POST se procesó. En un retiro, mirá status en el cuerpo: done, pending, error o duplicate.
400Pedido inválidoDatos mal formados o la operación no se puede hacer (saldo insuficiente, sin cuentas operativas). Corregí y volvé a intentar.
401Sin autenticarFalta la API key o es inválida.
403ProhibidoIP no habilitada (code: IP_NOT_WHITELISTED) o key sin el permiso necesario.
404No existeLa ruta o el método no existen. Revisá la URL (incluye /api/v1/external).
409ConflictoYa existe un retiro con esa idempotency_key. Consultá el original; no lo repitas con otra clave.
500Error internoEn consultas, reintentá con espera (1 s, 5 s, 30 s). En un retiro, no reintentes con otra clave: puede haber salido. Ver Retiros en duda.

Mensajes que podés recibir

Mensajes de error
messageHTTPEndpoint
API Key faltante401Todos con key
API Key inválida o revocada401Todos con key
IP no whitelisted403Todos con key
permisos insuficientes403Todos con key
el operador no tiene cuentas operativas400cbu · deposit/create · transfer
el monto debe ser mayor a cero400transfer
el CBU/CVU de destino tiene que tener 22 dígitos400transfer
saldo insuficiente para el retiro400transfer
ya existe un pago con esa idempotency_key409transfer
Internal server error500Cualquiera

Los textos pueden cambiar de redacción. Para automatizar, usá el código HTTP y, en los 403, el campo code. Los códigos de error de máquina para todos los casos están en Próximamente.

Idempotencia

Un mismo retiro nunca se paga dos veces, aunque repitas el pedido.

Los retiros (POST /external/transfer) llevan el campo idempotency_key en el cuerpo (no es un encabezado). Mandalo siempre, con un valor único y estable por retiro: lo ideal es el ID del retiro en tu sistema, por ejemplo RT-000398. Usá letras, números y guiones, hasta 64 caracteres.

La clave es obligatoria y es una por retiro

Dos retiros distintos nunca pueden compartir clave: el segundo responde duplicate y no se procesa, aunque vaya a otro destino o sea por otro monto. Antes de llamar, controlá en tu sistema que cada retiro lleve su propia clave.

Comportamiento de la idempotency_key
SituaciónRespuesta
Primera vez que usás la claveEl retiro se procesa normalmente.
Repetís la clave dentro de las 24 h (incluye un pedido que todavía está en curso)201 con {"status":"duplicate"}. No se hace nada nuevo.
Repetís la clave después de 24 h y el retiro original existe409 ya existe un pago con esa idempotency_key.
El retiro original fue rechazado (status: "error" o estado fallida)La clave queda libre: podés reintentar con la misma clave.

duplicate no dice en qué quedó el retiro original. Para saberlo, buscalo en GET /external/transactions: es la fila con tipo: "pago" cuyo idempotencyKey es tu clave o cuyo referenciaExterna empieza con tu clave. Mientras el retiro original está en curso, referenciaExterna viene en null: buscá por los dos campos.

Los depósitos esperados no son idempotentes

Cada POST /external/deposit/create crea un depósito nuevo. Si un pedido se corta, podés volver a pedirlo, pero el depósito anterior queda pendiente y, como se cruza por CUIT con el más viejo, puede conciliar el próximo cobro de ese jugador. Llevá en tu sistema qué depósitos pediste para cada jugador.

Conceptos

Lo que tenés que saber antes de integrar.

Cuentas CVU

Son las cuentas a nombre de tu empresa donde tus jugadores transfieren. Cada una tiene un CVU de 22 dígitos y, opcionalmente, un alias. Las ves con GET /external/balance, con el saldo de cada una; el saldo_real total es la suma de todas.

Rotación de CVU

Cada vez que pedís un CVU (con GET /external/cbu o al crear un depósito esperado) el sistema elige una de tus cuentas CVU operativas, así los cobros se reparten entre todas. Hoy el criterio es la cuenta operativa con menor saldo.

  • No guardes un CVU fijo por jugador ni lo muestres en tu sitio de forma permanente: pedilo en cada depósito.
  • Si no tenés ninguna cuenta operativa, la API responde 400 el operador no tiene cuentas operativas.
  • La rotación por tope mensual (que cada cuenta deje de asignarse al acercarse a su tope del mes) está en Próximamente.

Depósito esperado y conciliación por CUIT

Antes de que el jugador transfiera, tu sistema registra un depósito esperado con el monto y el CUIT o CUIL del jugador (POST /external/deposit/create). Cuando entra una transferencia, se busca un depósito pendiente con el mismo CUIT del que transfirió y se compara el monto:

Resultado de la conciliación
Qué llegóEventoQué hacer en tu sistema
El monto esperado, exactotransaction.confirmedYa está en tu saldo: acreditale el depósito al jugador por data.monto.
Menos de lo esperado
motivo monto_menor
transaction.in_reviewNo lo acredites: queda retenido y no suma a tu saldo. Esperá cómo lo resuelve tu equipo (ver abajo).
Más de lo esperado
motivo monto_mayor
transaction.in_reviewNo lo acredites: queda retenido y no suma a tu saldo. Esperá cómo lo resuelve tu equipo (ver abajo).
Una transferencia de un CUIT sin depósito pendiente
motivo sin_deposito
transaction.in_reviewNo lo acredites: queda retenido y no suma a tu saldo. Esperá cómo lo resuelve tu equipo (ver abajo).
  • Se acredita automáticamente solo el cobro que coincide con su depósito esperado. Los demás quedan retenidos: ningún movimiento dudoso se acredita sin revisión. Tu equipo los resuelve en A revisar del panel y te llega transaction.confirmed (con el monto que se acreditó) o transaction.refunded (se devolvió a quien transfirió), con el mismo data.id.
  • También quedan retenidos un CUIT casi igual al esperado (motivo cuit_distinto) y una transferencia repetida del mismo CUIT y monto en pocos segundos (posible_duplicado).
  • El cruce es solo por CUIT, no por monto ni por la cuenta CVU de destino. Si hay varios depósitos pendientes del mismo CUIT, se usa el más viejo. Cada depósito concilia un solo cobro.
  • El CUIT tiene que ser el del titular de la cuenta desde la que transfiere el jugador. Si transfiere desde la cuenta de otra persona, el cobro queda retenido y te llega transaction.in_review. Avisale al jugador en tu pantalla de depósito.
  • Mandá el CUIT con 11 dígitos y sin guiones (27402187354), igual que como lo informa el banco.
  • Un cobro con un código de transferencia ya registrado no se acredita dos veces.
  • Vencimiento: expires_at es informativo. Hoy un depósito pendiente sigue vigente después de esa hora y puede conciliar un cobro posterior. El vencimiento automático está en Próximamente (cómo va a funcionar: ver Próximamente).

Tipo de tráfico: FTD y STD

Al crear un depósito esperado podés marcar el tipo de tráfico en traffic_type. Se guarda en la transacción conciliada (campo trafficType) para que separes en tus reportes los primeros depósitos del resto. No cambia cómo se concilia ni cuánto se acredita.

Tipos de tráfico
ValorSignificado
ftdFirst time deposit: el primer depósito del jugador.
stdStandard deposit: los depósitos siguientes. Es el valor por defecto.

Mandá solo ftd o std, en minúsculas.

Estados

Campo status de cada fila de GET /external/transactions. Los cobros (tipo: "cobro") están confirmados (con la acreditación automática, que está en Próximamente, un cobro retenido va a figurar en_revision y uno devuelto, devuelta); los retiros (tipo: "pago") pasan por estos estados:

Estados de transacciones
statusAplica aQué significaSaldo
confirmada ConfirmadaCobrosEl cobro entró y está acreditado a tu saldo.Acreditado
confirmada ConfirmadaRetirosEl retiro se ejecutó (al momento o confirmado después, en la verificación posterior).Debitado
iniciada En cursoRetirosSaldo reservado y retiro en ejecución. Si queda así, pasa a la verificación posterior: se confirma o va a revisión.Reservado
pendiente PendienteRetirosEl retiro fue aceptado y espera la confirmación de la ejecución.Debitado
en_duda En dudaRetirosNo hubo respuesta clara: puede haber salido o no. Se verifica después contra los movimientos de la cuenta CVU.Reservado (no se devuelve)
requiere_revision En revisiónRetirosPasaron más de 10 minutos sin poder confirmarlo. No se resuelve solo ni genera un aviso: escribinos a soporte. Mientras tanto se sigue verificando.Reservado (no se devuelve)
fallida FallidaRetirosRechazado sin ejecutarse.Devuelto

El status de la respuesta de POST /external/transfer y el del aviso withdrawal.* usan otros valores. Así se relacionan:

Equivalencias de estados de retiro
Respuesta de /transferAvisostatus en el avisoEstado en transacciones
201 · donewithdrawal.completeddoneconfirmada
201 · pending (hoy no se produce)withdrawal.pendingpendingpendiente
201 · error (hoy no se produce)withdrawal.failederrorfallida
500withdrawal.failederrorfallida
500 (o sin respuesta)withdrawal.pendingunknownen_duda → requiere_revision
—withdrawal.completed con conciliado: truedoneconfirmada
201 · duplicateNinguno—El del retiro original

Retiros en duda o en revisión: qué hacer

Un retiro queda en duda cuando no se sabe si salió: tu pedido recibió 500, se cortó por tiempo o llegó el aviso withdrawal.pending con status: "unknown". El saldo queda reservado mientras se verifica, para que nunca se pague dos veces.

  1. Marcalo como "en verificación" en tu sistema y no se lo informes al jugador como fallido.
  2. Esperá el aviso. Si se confirma, te llega withdrawal.completed con conciliado: true. Un retiro en duda nunca vuelve solo a tu saldo ni termina en withdrawal.failed: si no se puede confirmar, pasa a requiere_revision, sin aviso.
  3. Si querés reintentar, usá la misma idempotency_key. Si el original sigue en duda vas a recibir duplicate; si había fallado, se procesa de nuevo de forma segura.
  4. Consultá el estado en GET /external/transactions: la fila de tipo: "pago" con tu clave en idempotencyKey o al principio de referenciaExterna.
  5. Si pasa a requiere_revision (más de 10 minutos sin confirmación), escribinos a soporte con tu idempotency_key, el CBU y el monto. No se resuelve solo y este cambio no genera ningún aviso: hasta que soporte lo cierre, el saldo queda reservado.

Referencia de endpoints

Todas las rutas son relativas a https://api.luckycobros.com.ar/api/v1.

Resumen de endpoints
MétodoRutaPermisoPara qué
GET/health—Estado del servicio.
GET/external/balancereadSaldo total y por cuenta CVU.
GET/external/cbureadPedir un CVU para recibir un cobro.
GET/external/transactionsreadListado de cobros y retiros, paginado.
POST/external/deposit/createwriteCrear un depósito esperado y obtener su CVU.
POST/external/transferwritePedir un retiro a un CBU o CVU.
POST/external/transfer/verifywriteValidar un destino antes de un retiro.
GET/external/webhookreadLeer la configuración del webhook.
PUT/external/webhookwriteConfigurar la URL y el secreto del webhook.
GET/health

Estado del servicio

Sin API key

Responde si la API está en línea. No lleva API key ni toca datos de tu cuenta: usalo para tu monitoreo o antes de un proceso por lotes.

Ejemplo

curl "https://api.luckycobros.com.ar/api/v1/health"

Respuesta

200Respuesta
{
  "status": "ok"
}
GET/external/balance

Consultar saldo

Permiso read

Devuelve el saldo total y el de cada una de tus cuentas CVU. No tiene parámetros.

Ejemplo

curl "https://api.luckycobros.com.ar/api/v1/external/balance" \
  -H "X-API-Key: $LUCKY_API_KEY"

Respuesta

200Respuesta
{
  "saldo_real": 8942438,
  "saldo_neto": 8942438,
  "cantidad_cuentas": 4,
  "cuentas": [
    {
      "id": "7b1f2c4e-9a0d-4c55-8f3e-2d6a1b9c0e11",
      "cvu": "0000003100047812369019",
      "alias": "rio.cobros.central1",
      "saldo_real": 4812345.6
    },
    {
      "id": "c3d9a8b2-1e4f-4a7b-9c6d-5e8f7a6b4c22",
      "cvu": "0000003100052290418737",
      "alias": "rio.cobros.central2",
      "saldo_real": 2106780.25
    },
    {
      "id": "e5a7c9d1-3f2b-4d6e-8a0c-9b1d3f5e7a33",
      "cvu": "0000003100061174205583",
      "alias": "rio.cobros.rosario",
      "saldo_real": 1384902.15
    },
    {
      "id": "a1c3e5f7-9b2d-4f6a-8c0e-2d4f6a8c0e44",
      "cvu": "0000003100078836512094",
      "alias": "rio.cobros.mendoza",
      "saldo_real": 638410
    }
  ]
}
Campos de balance
CampoTipoDescripción
saldo_realnumberSuma del saldo de todas tus cuentas CVU, en ARS.
saldo_netonumberHoy es igual a saldo_real. Cuando se descuenten comisiones del saldo, va a mostrar lo disponible después de comisiones (Próximamente).
cantidad_cuentasnumberCantidad de cuentas CVU.
cuentas[].idstring (UUID)Identificador de la cuenta (es el cuentaId de las transacciones).
cuentas[].cvustringCVU de 22 dígitos.
cuentas[].aliasstring · nullAlias de la cuenta, si tiene.
cuentas[].saldo_realnumberSaldo de esa cuenta.

Si todavía no tenés cuentas, responde {"saldo_real":0,"saldo_neto":0,"cantidad_cuentas":0,"cuentas":[]}.

Errores

Errores
HTTPmessageQué pasó / qué hacer
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.
GET/external/cbu

Pedir un CVU para cobrar

Permiso read

Devuelve la cuenta CVU que corresponde según la rotación, para mostrársela al jugador. Si además querés que el cobro se concilie con un pedido tuyo, usá POST /external/deposit/create, que elige la cuenta y registra el depósito esperado en un solo paso.

Ejemplo

curl "https://api.luckycobros.com.ar/api/v1/external/cbu" \
  -H "X-API-Key: $LUCKY_API_KEY"

Respuesta

200Respuesta
{
  "cvu": "0000003100052290418737",
  "cbu": null,
  "alias": "rio.cobros.central2",
  "titular": "RIO GAMING S.A.",
  "moneda": "ARS"
}
Campos de cbu
CampoTipoDescripción
cvustringCVU de 22 dígitos al que tiene que transferir el jugador.
cbunullReservado. Hoy siempre null: usá cvu.
aliasstring · nullAlias de la cuenta, si tiene.
titularstring · nullTitular de la cuenta (tu razón social).
monedastringSiempre ARS.

Errores

Errores
HTTPmessageQué pasó / qué hacer
400el operador no tiene cuentas operativasNo hay ninguna cuenta CVU operativa para asignar. Revisá tus cuentas en el panel.
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.
GET/external/transactions

Listar transacciones

Permiso read

Lista cobros y retiros juntos, del más nuevo al más viejo. Usalo para conciliar contra tu sistema al final del día y para consultar el estado de un retiro. Hoy no tiene filtros: recorré las páginas hasta la fecha que necesites.

Pedido

Parámetros de transactions
CampoTipoObligatorioDescripción
pageintegerNoPágina, desde 1. Por defecto 1.
limitintegerNoFilas por página. Por defecto 20. Recomendado: hasta 100 (hoy no hay un tope fijo, pero las páginas grandes tardan más). Mandá siempre enteros positivos.

Ejemplo

curl "https://api.luckycobros.com.ar/api/v1/external/transactions?page=1&limit=3" \
  -H "X-API-Key: $LUCKY_API_KEY"

Respuesta

200Respuesta
{
  "data": [
    {
      "id": "0f5c1e7a-3b2d-4e8f-9a6c-1d2e3f4a5b6c",
      "operadorId": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
      "cuentaId": "7b1f2c4e-9a0d-4c55-8f3e-2d6a1b9c0e11",
      "codigoCoelsa": "MP36D4CPEC9G",
      "idempotencyKey": null,
      "referenciaExterna": null,
      "tipo": "cobro",
      "monto": "25000",
      "montoEsperado": "25000",
      "status": "confirmada",
      "trafficType": "ftd",
      "contraparteNombre": "MICAELA SOSA",
      "contraparteCuit": "27402187354",
      "contraparteCbu": null,
      "isDelayed": false,
      "createdAt": "2026-10-28T17:03:11.482Z"
    },
    {
      "id": "9e8d7c6b-5a4f-4e3d-8c1b-0a9f8e7d6c5b",
      "operadorId": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
      "cuentaId": "7b1f2c4e-9a0d-4c55-8f3e-2d6a1b9c0e11",
      "codigoCoelsa": "N4QX8T2LMC5R",
      "idempotencyKey": "RT-000398",
      "referenciaExterna": "RT-000398",
      "tipo": "pago",
      "monto": "1500000",
      "montoEsperado": null,
      "status": "confirmada",
      "trafficType": null,
      "contraparteNombre": null,
      "contraparteCuit": null,
      "contraparteCbu": "0070999830004123456781",
      "isDelayed": false,
      "createdAt": "2026-10-28T16:20:09.317Z"
    },
    {
      "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
      "operadorId": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
      "cuentaId": "7b1f2c4e-9a0d-4c55-8f3e-2d6a1b9c0e11",
      "codigoCoelsa": null,
      "idempotencyKey": null,
      "referenciaExterna": "RT-000394",
      "tipo": "pago",
      "monto": "92500",
      "montoEsperado": null,
      "status": "fallida",
      "trafficType": null,
      "contraparteNombre": null,
      "contraparteCuit": null,
      "contraparteCbu": "2850590940090418135209",
      "isDelayed": false,
      "createdAt": "2026-10-27T18:42:30.904Z"
    }
  ],
  "pagination": {
    "total": 8353,
    "page": 1,
    "limit": 3,
    "totalPages": 2785,
    "hasMore": true
  }
}

Ojo con el formato: en este endpoint los campos van en camelCase y los montos llegan como texto. pagination.totalPages es total / limit redondeado hacia arriba y hasMore indica si hay otra página.

Campos de una transacción
CampoTipoDescripción
idstring (UUID)Identificador de la transacción. Es el mismo data.id de los avisos de cobro.
operadorIdstring (UUID)Identificador de tu empresa.
cuentaIdstring (UUID) · nullCuenta CVU donde entró el cobro o de la que salió el retiro (ver cuentas[].id en balance). Puede venir en null.
codigoCoelsastring · nullCódigo de la transferencia en la red interbancaria. En retiros puede ser null si no se ejecutó.
idempotencyKeystring · nullSolo retiros: tu idempotency_key. Queda en null si el retiro falló, para que puedas reutilizarla.
referenciaExternastring · nullSolo retiros: tu idempotency_key, cuando el retiro se cerró al pedirlo (también si falló). Mientras está iniciada viene en null, y sigue así si después se confirma o pasa a revisión desde ese estado: buscá por idempotencyKey o por este campo. En casos excepcionales lleva un sufijo después de un espacio: buscá por prefijo.
tipostringcobro o pago (retiro).
montostring decimalMonto en ARS, como texto: "25000", "92500.5".
montoEsperadostring decimal · nullSolo cobros conciliados con un depósito esperado: el monto que esperabas.
statusstringVer Estados.
trafficTypestring · nullftd o std, si el cobro se concilió con un depósito esperado. Se guarda el valor que mandaste en traffic_type.
contraparteNombrestring · nullCobros: nombre del titular que transfirió. Puede venir vacío ("").
contraparteCuitstring · nullCobros: CUIT o CUIL del titular que transfirió, normalmente de 11 dígitos. Puede venir vacío ("") si no se informó.
contraparteCbustring · nullRetiros: CBU o CVU de destino.
isDelayedbooleanReservado. Hoy siempre false.
createdAtstring (ISO 8601 UTC)Fecha y hora de alta.

Errores

Errores
HTTPmessageQué pasó / qué hacer
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.
POST/external/deposit/create

Crear un depósito esperado

Permiso write

Registra que esperás un cobro de un jugador y te devuelve la cuenta CVU a la que tiene que transferir. Cuando entre una transferencia de ese CUIT, se concilia con este depósito y te llega el aviso transaction.confirmed, o transaction.in_review si algo no coincide. Ver cómo se concilia.

Pedido

Cuerpo de deposit/create
CampoTipoObligatorioDescripción
montonumberSíMonto que esperás, en ARS. Tiene que ser mayor a cero.
contraparte_cuil_cuitstringSí (o cuit)CUIT o CUIL del jugador, 11 dígitos sin guiones. Tiene que ser el titular de la cuenta desde la que va a transferir.
cuitstringNoAlternativa a contraparte_cuil_cuit (se usa si ese falta).
traffic_typestringNoftd o std. Por defecto std. Ver Tipo de tráfico.
ttl_minutesintegerNoMinutos de vigencia que querés informar. Por defecto 30. Hoy se usa para calcular expires_at, que es informativo.

Ejemplo

curl -X POST "https://api.luckycobros.com.ar/api/v1/external/deposit/create" \
  -H "X-API-Key: $LUCKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "monto": 25000,
    "contraparte_cuil_cuit": "27402187354",
    "traffic_type": "ftd",
    "ttl_minutes": 30
  }'

Respuesta

201Respuesta
{
  "id": "3c2b1a09-8f7e-4d6c-9b4a-39281706f5e4",
  "cvu": "0000003100047812369019",
  "expires_at": "2026-10-28T17:33:11.482Z"
}
Campos de deposit/create
CampoTipoDescripción
idstring (UUID)Identificador del depósito esperado. Guardalo junto a tu pedido.
cvustringCVU que tenés que mostrarle al jugador. Mostrá este, no uno pedido antes con /cbu.
expires_atstring (ISO 8601 UTC)Vigencia informada: alta + ttl_minutes. Hoy no vence solo.

Validá los datos antes de enviar

Mandá siempre monto como número mayor a cero y el CUIT o CUIL del jugador con 11 dígitos, sin guiones: es el dato con el que se concilia el cobro, así que con un CUIT incompleto o equivocado el cobro no se cruza con este depósito. Este pedido no es idempotente: ver Idempotencia.

Errores

Errores
HTTPmessageQué pasó / qué hacer
400el operador no tiene cuentas operativasNo hay cuentas CVU operativas para asignar.
500Internal server errorError interno. Revisá que el pedido tenga los campos obligatorios con su tipo y reintentá; si sigue, escribinos a soporte.
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.
POST/external/transfer

Pedir un retiro

Permiso write

Transfiere desde tu saldo a cualquier CBU o CVU. El retiro sale de una sola de tus cuentas CVU: la de mayor saldo. Si ninguna cuenta alcanza por sí sola, responde 400 saldo insuficiente para el retiro aunque la suma de tus cuentas alcance.

  • Guardá el retiro en tu base antes de llamar. El aviso withdrawal.* se envía antes de que termine esta respuesta, así que normalmente te llega primero: tu receptor lo tiene que poder cruzar sin la respuesta, y la respuesta no tiene que pisar el estado que ya fijó el aviso.
  • Esperá hasta 60 segundos la respuesta. La respuesta sale recién cuando tu receptor contesta el aviso del retiro: si tu receptor tarda, este pedido también tarda y puede pasar ese plazo. Por eso, respondé los avisos en menos de 2 segundos. Si se corta, tratalo como en duda: no cambies la clave.
  • Opcional: validá el destino antes con /transfer/verify.

Pedido

Cuerpo de transfer
CampoTipoObligatorioDescripción
cbustringSíCBU o CVU de destino. Se aceptan espacios y guiones, pero tiene que quedar en 22 dígitos.
cbu_cvustringNoAlternativa a cbu (se usa si ese viene vacío).
montonumberSíMonto en ARS, mayor a cero, con hasta 2 decimales.
conceptostringNoDescripción de la transferencia.
idempotency_keystringSíClave única y estable por retiro (el ID en tu sistema), de hasta 64 caracteres. Ver Idempotencia.

Ejemplo

curl -X POST "https://api.luckycobros.com.ar/api/v1/external/transfer" \
  -H "X-API-Key: $LUCKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cbu": "0070999830004123456781",
    "monto": 1500000,
    "concepto": "Fondeo cuenta operativa",
    "idempotency_key": "RT-000398"
  }'

Respuesta

{
  "status": "done",
  "coelsaId": "N4QX8T2LMC5R",
  "event": "withdrawal.completed"
}

La respuesta trae coelsaId (en camelCase) y el nombre del aviso que se envió en event. No incluye el id de la transacción ni tu clave. coelsaId identifica la operación, pero puede venir vacío o con un identificador que no es el código interbancario: no lo uses como clave única. Las respuestas 201 · pending y 201 · error son parte del contrato, pero hoy no se producen: un retiro hoy termina en done, duplicate, 400, 409 o 500. Manejalas igual, porque pueden empezar a aparecer sin cambio de versión.

Resultados de un retiro
ResultadoHTTP · statusAvisoSaldoQué hacés
Ejecutado201 · donewithdrawal.completedDebitadoMarcalo como pagado.
Aceptado, sin confirmar (hoy no se produce)201 · pendingwithdrawal.pendingDebitadoEsperá withdrawal.completed.
Rechazado (hoy no se produce)201 · errorwithdrawal.failedDevueltoPodés reintentar con la misma clave.
Error interno o sin respuesta500 o sin respuestawithdrawal.failed (status: "error") o withdrawal.pending (status: "unknown")Devuelto o reservado, según el avisoNo cambies la clave. El aviso te dice cómo terminó. Ver Retiros en duda.
Clave repetida (24 h)201 · duplicateNingunoSin cambiosConsultá el original en transacciones.
Clave repetida (después de 24 h)409NingunoSin cambiosConsultá el original en transacciones.
Datos inválidos o sin saldo400NingunoSin cambiosCorregí y reintentá (la clave queda libre).

Ante un error interno, esperá el aviso

Ante un 500 o un corte, el retiro puede no haberse hecho (y la plata vuelve a tu saldo) o haber quedado en duda. Los distingue el aviso: withdrawal.failed o withdrawal.pending con status: "unknown". En los dos casos, reintentá solo con la misma clave.

Errores

Errores
HTTPmessageQué pasó / qué hacer
400el monto debe ser mayor a ceroFalta monto, no es numérico o es cero o negativo.
400el CBU/CVU de destino tiene que tener 22 dígitosEl destino no tiene 22 dígitos después de sacar espacios y guiones.
400el operador no tiene cuentas operativasNo hay cuentas CVU operativas.
400saldo insuficiente para el retiroNinguna cuenta CVU tiene saldo suficiente por sí sola.
409ya existe un pago con esa idempotency_keyLa clave ya se usó en un retiro anterior.
500Internal server errorError interno. No cambies la clave: el aviso te dice cómo terminó. Ver arriba.
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.
POST/external/transfer/verify

Validar un destino

Permiso write

Antes de un retiro, revisa el CBU o CVU de destino. Siempre te dice si el formato es válido; si el destino ya es conocido por tus cuentas, además te devuelve el titular y su CUIT para que se lo muestres a quien pide el retiro.

valid: true significa que el formato es correcto. No confirma que la cuenta exista: si no viene el titular, pedile al usuario que lo confirme.

Pedido

Cuerpo de transfer/verify
CampoTipoObligatorioDescripción
cbustringSíCBU o CVU, 22 dígitos seguidos, sin espacios ni guiones (acá no se limpian).
cbu_cvustringNoAlternativa a cbu.

Ejemplo

curl -X POST "https://api.luckycobros.com.ar/api/v1/external/transfer/verify" \
  -H "X-API-Key: $LUCKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cbu": "0070999830004123456781"
  }'

Respuesta

{
  "holderName": "RIO GAMING S.A.",
  "cuit": "30715832649",
  "valid": true
}
Campos de transfer/verify
CampoTipoDescripción
holderNamestring · nullTitular del destino, si es conocido.
cuitstring · nullCUIT del titular, si es conocido.
validbooleantrue si el destino tiene 22 dígitos.

Errores

Errores
HTTPmessageQué pasó / qué hacer
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.

Un CBU mal escrito no da error: responde 201 con valid: false. Este endpoint pide permiso write.

GET/external/webhook

Leer la configuración del webhook

Permiso read

Devuelve la URL donde recibís los avisos y el secreto con el que se firman. Si no configuraste nada, los dos campos vienen en null.

Ejemplo

curl "https://api.luckycobros.com.ar/api/v1/external/webhook" \
  -H "X-API-Key: $LUCKY_API_KEY"

Respuesta

{
  "callback_url": "https://tu-sistema.example.com/webhooks/cobros",
  "webhook_secret": "PEGA_ACA_TU_SECRETO_DE_64_CARACTERES"
}

La respuesta incluye el secreto completo

Cualquier key con permiso read puede leerlo. Restringí esas keys por IP y no las compartas con sistemas de reportes de terceros.

Errores

Errores
HTTPmessageQué pasó / qué hacer
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.
PUT/external/webhook

Configurar el webhook

Permiso write

Define la URL donde recibís los avisos y el secreto con el que se firman. Hay una sola URL por empresa, para todos los eventos.

Pedido

Cuerpo de webhook
CampoTipoObligatorioDescripción
callback_urlstringLa primera vezURL pública de tu sistema que recibe los avisos por POST. Usá https://. Si no la mandás, se mantiene la anterior. Si la mandás en null, se borra y dejás de recibir avisos.
webhook_secretstringLa primera vezSecreto para firmar los avisos. Lo elegís vos: usá al menos 32 caracteres al azar (openssl rand -hex 32). Si lo mandás vacío o no lo mandás, se mantiene el anterior.

Ejemplo

curl -X PUT "https://api.luckycobros.com.ar/api/v1/external/webhook" \
  -H "X-API-Key: $LUCKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://tu-sistema.example.com/webhooks/cobros",
    "webhook_secret": "PEGA_ACA_TU_SECRETO_DE_64_CARACTERES"
  }'

Respuesta

200Respuesta
{
  "callback_url": "https://tu-sistema.example.com/webhooks/cobros",
  "webhook_secret": "PEGA_ACA_TU_SECRETO_DE_64_CARACTERES"
}

Errores

Errores
HTTPmessageQué pasó / qué hacer
401API Key faltanteNo mandaste el encabezado X-API-Key.
401API Key inválida o revocadaLa key no existe o fue revocada.
403IP no whitelistedEl pedido salió de una IP que no está en la lista de la key.
403permisos insuficientesLa key no tiene el permiso que pide el endpoint.

Webhooks

Avisos firmados que llegan a tu sistema con cada cobro y cada cambio de un retiro.

Configuración

  1. Generá un secreto al azar: openssl rand -hex 32.
  2. Configurá tu URL y el secreto con PUT /external/webhook (cuando tengas el panel, también desde Integración).
  3. Publicá un endpoint POST en esa URL que verifique la firma y responda 2xx. Tenés ejemplos listos en Verificar la firma.

Hay una sola URL por empresa: todos los eventos llegan ahí y se distinguen por el campo event.

Formato del aviso

Cada aviso es un POST con un JSON {"event": "…", "data": {…}} y estos encabezados:

Encabezados del aviso
EncabezadoEjemploDescripción
Content-Typeapplication/jsonSiempre JSON en UTF-8.
X-Webhook-Signaturesha256=5f0c…HMAC-SHA256 del cuerpo crudo con tu webhook_secret, en hexadecimal minúscula, con el prefijo sha256=.
X-Webhook-Eventtransaction.confirmedNombre del evento (igual que event en el cuerpo).
X-Webhook-Timestamp2026-10-28T17:03:11.482ZHora de envío en UTC. No forma parte de la firma: usala solo como referencia.

Eventos

Eventos
EventoCuándo se envía
transaction.confirmed PróximamenteEl cobro se acreditó a tu saldo: coincidió con su depósito esperado o tu equipo lo aprobó desde A revisar. Es el único aviso de cobro que se acredita.
transaction.in_review PróximamenteEntró un cobro que no coincide (monto menor o mayor, sin depósito esperado, CUIT distinto o posible duplicado). Queda retenido: no se acredita hasta que tu equipo lo resuelva. El motivo viaja en data.motivo.
transaction.refunded PróximamenteTu equipo devolvió un cobro retenido a la cuenta de origen. No se acredita.
withdrawal.completedEl retiro se ejecutó. Con conciliado: true si se confirmó en la verificación posterior de un retiro pendiente o en duda.
withdrawal.pendingEl retiro fue aceptado sin confirmar (status: "pending") o su resultado es desconocido (status: "unknown").
withdrawal.failedEl retiro fue rechazado al pedirlo y la plata volvió a tu saldo. Un retiro que quedó en duda nunca termina en este aviso.

Cobros: transaction.*

Estos avisos llegan con la acreditación automática de cobros

Están en Próximamente: hoy no se envían. El formato de abajo es el definitivo, así que podés dejar tu receptor listo y probarlo con el caso de firma.

{
  "event": "transaction.confirmed",
  "data": {
    "id": "0f5c1e7a-3b2d-4e8f-9a6c-1d2e3f4a5b6c",
    "codigo_coelsa": "MP36D4CPEC9G",
    "monto": 25000,
    "contraparte_nombre": "MICAELA SOSA",
    "contraparte_cuil_cuit": "27402187354",
    "status": "confirmada"
  }
}
Campos de los avisos de cobro
Campo de dataTipoDescripción
idstring (UUID)Identificador de la transacción (el id de GET /external/transactions). Es el mismo en todos los avisos de un cobro: deduplicá por event + id.
codigo_coelsastringCódigo de la transferencia en la red interbancaria.
montonumberMonto que llegó de verdad, en ARS. En transaction.confirmed es lo que se acreditó a tu saldo y lo que tenés que acreditarle al jugador.
contraparte_nombrestring · nullTitular de la cuenta que transfirió.
contraparte_cuil_cuitstring · nullCUIT o CUIL de quien transfirió, 11 dígitos. Con este dato encontrás el depósito esperado en tu sistema.
statusstringconfirmada (acreditado a tu saldo), en_revision (retenido, sin acreditar) o devuelta.
motivostringSolo en in_review y refunded: por qué quedó retenido. monto_menor, monto_mayor, sin_deposito, cuit_distinto o posible_duplicado.

El aviso no trae el id del depósito esperado, ni el monto esperado, ni el tipo de tráfico. Para cruzarlo con tu pedido, buscá en tu sistema el depósito pendiente más viejo de ese CUIT: es la misma regla que usamos para conciliar. Para que dé el mismo resultado de los dos lados, no des por vencidos ni canceles depósitos pendientes por expires_at mientras el vencimiento esté en Próximamente: para nosotros siguen pendientes. Si igual los vencés, cruzá cada cobro con el montoEsperado de su transacción en GET /external/transactions. El detalle completo está en GET /external/transactions.

Retiros: withdrawal.*

{
  "event": "withdrawal.completed",
  "data": {
    "coelsa_id": "N4QX8T2LMC5R",
    "monto": 1500000,
    "cbu": "0070999830004123456781",
    "status": "done"
  }
}
Campos de los avisos de retiro
Campo de dataTipoDescripción
coelsa_idstringCódigo de la transferencia. No viene cuando el retiro se rechazó antes de ejecutarse ni cuando su resultado es desconocido.
montonumberMonto del retiro, tal como lo pediste.
cbustringCBU o CVU de destino, 22 dígitos.
statusstringdone, pending, unknown o error.
conciliadobooleanSolo en withdrawal.completed: true si se confirmó en la verificación posterior.

Cómo cruzar un aviso de retiro con tu pedido

El aviso no trae tu idempotency_key. Cruzalo por cbu + monto entre tus retiros abiertos: el aviso suele llegar antes que la respuesta de /transfer, así que todavía no tenés el coelsa_id. Para que el cruce sea siempre único, evitá tener dos retiros abiertos al mismo destino por el mismo monto.

Verificar la firma

Calculá el HMAC-SHA256 del cuerpo crudo, tal cual llegó, con tu webhook_secret como clave; pasalo a hexadecimal y comparalo en tiempo constante con lo que viene después de sha256= en X-Webhook-Signature. Si no coincide, respondé 401 y descartalo.

Verificá sobre el cuerpo crudo, no sobre el JSON rearmado

Si parseás el JSON y lo volvés a serializar, cambian los espacios o el orden de las claves y la firma no coincide. Leé los bytes del pedido antes de parsearlos.

const crypto = require('node:crypto');

// cuerpoCrudo: Buffer con el cuerpo tal cual llegó (antes de cualquier JSON.parse)
function firmaValida(secreto, cuerpoCrudo, encabezado) {
  if (typeof encabezado !== 'string') return false;
  const recibida = encabezado.trim().replace(/^sha256=/i, '').toLowerCase();
  if (!/^[0-9a-f]{64}$/.test(recibida)) return false;
  const esperada = crypto.createHmac('sha256', secreto).update(cuerpoCrudo).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(recibida, 'hex'), Buffer.from(esperada, 'hex'));
}

// Con Express: app.post('/webhooks/cobros', express.raw({ type: 'application/json' }), (req, res) => {
//   if (!firmaValida(process.env.WEBHOOK_SECRET, req.body, req.get('X-Webhook-Signature'))) return res.sendStatus(401);
//   res.sendStatus(200);  // respondé primero, procesá después
// });

Probá tu verificación con este caso real

Con el secreto whsec_prueba_123 y exactamente este cuerpo (una sola línea, sin espacios), la firma tiene que darte el valor de abajo:

Cuerpo crudo
{"event":"transaction.confirmed","data":{"id":"0f5c1e7a-3b2d-4e8f-9a6c-1d2e3f4a5b6c","codigo_coelsa":"MP36D4CPEC9G","monto":25000,"contraparte_nombre":"MICAELA SOSA","contraparte_cuil_cuit":"27402187354","status":"confirmada"}}
X-Webhook-Signature esperado
sha256=a548acee9bd1cf70845e50e014fc73d7c57a36650049eb5e1098fd76e4b6029f

Servidores de ejemplo completos

Reciben el aviso, verifican la firma, responden 200 enseguida, deduplican y procesan. Están listos para correr, sin dependencias:

// Lucky Cobros · servidor mínimo que recibe los avisos (webhooks) y verifica la firma.
//
// Uso (Node 18 o superior, sin dependencias):
//   WEBHOOK_SECRET="el-mismo-secreto-que-configuraste" node verificar-firma.js
// Escucha en http://localhost:3000/webhooks/cobros (cambiá el puerto con PORT=8080).
//
// Reglas que sigue este ejemplo:
//   1. La firma se calcula sobre el cuerpo CRUDO, byte por byte. No lo parsees ni lo vuelvas a serializar antes.
//   2. La comparación es en tiempo constante (crypto.timingSafeEqual).
//   3. Responde 200 enseguida y procesa después: tu demora retrasa el resto de la operación.
//   4. Deduplica: el mismo aviso no se procesa dos veces.
'use strict';

const http = require('node:http');
const crypto = require('node:crypto');

const SECRETO = process.env.WEBHOOK_SECRET;
const PUERTO = Number(process.env.PORT) || 3000;
const RUTA = '/webhooks/cobros';

if (!SECRETO) {
  console.error('Falta WEBHOOK_SECRET: usá el mismo secreto que configuraste con PUT /external/webhook.');
  process.exit(1);
}

/** true si el encabezado X-Webhook-Signature corresponde a este cuerpo y este secreto. */
function firmaValida(secreto, cuerpoCrudo, encabezado) {
  if (typeof encabezado !== 'string') return false;
  const recibida = encabezado.trim().replace(/^sha256=/i, '').toLowerCase();
  if (!/^[0-9a-f]{64}$/.test(recibida)) return false;
  const esperada = crypto.createHmac('sha256', secreto).update(cuerpoCrudo).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(recibida, 'hex'), Buffer.from(esperada, 'hex'));
}

/** Clave para no procesar dos veces el mismo aviso (null = no hay código para deduplicar). */
function claveDeDuplicado(aviso) {
  const d = aviso.data || {};
  // Un mismo cobro puede recibir transaction.in_review y después confirmed o refunded: la clave lleva el evento.
  if (aviso.event.startsWith('transaction.')) return d.id ? aviso.event + ':' + d.id : null;
  if (aviso.event.startsWith('withdrawal.')) return d.coelsa_id ? aviso.event + ':' + d.coelsa_id : null;
  return null;
}

// En producción guardá las claves en tu base, con un índice único. Acá alcanza con memoria.
const procesados = new Set();

function procesar(aviso) {
  const d = aviso.data;
  switch (aviso.event) {
    case 'transaction.confirmed':
      // Ya está en tu saldo: acreditá d.monto al jugador con CUIT d.contraparte_cuil_cuit.
      console.log(`[cobro] ${aviso.event} · acreditar $ ${d.monto} de ${d.contraparte_nombre} (${d.contraparte_cuil_cuit}) · código ${d.codigo_coelsa}`);
      break;
    case 'transaction.in_review':
    case 'transaction.refunded':
      // No acredites. in_review: queda retenido hasta que tu equipo lo resuelva en el panel (después llega
      // confirmed o refunded con el mismo data.id). refunded: se le devolvió a quien transfirió.
      console.log(`[cobro] ${aviso.event} · no se acredita · $ ${d.monto} de ${d.contraparte_nombre} · motivo ${d.motivo || '-'}`);
      break;
    case 'withdrawal.completed':
    case 'withdrawal.pending':
    case 'withdrawal.failed':
      // Buscá tu retiro abierto por cbu + monto (o por coelsa_id si ya lo tenías) y actualizá su estado.
      console.log(`[retiro] ${aviso.event} · status ${d.status} · $ ${d.monto} a ${d.cbu}${d.coelsa_id ? ' · código ' + d.coelsa_id : ''}`);
      break;
    default:
      console.log(`[aviso] evento nuevo, lo ignoramos: ${aviso.event}`);
  }
}

const servidor = http.createServer((req, res) => {
  const ruta = (req.url || '').split('?')[0];
  if (req.method !== 'POST' || ruta !== RUTA) {
    res.writeHead(404).end();
    return;
  }

  const partes = [];
  req.on('data', (parte) => partes.push(parte));
  req.on('end', () => {
    const cuerpoCrudo = Buffer.concat(partes);

    if (!firmaValida(SECRETO, cuerpoCrudo, req.headers['x-webhook-signature'])) {
      res.writeHead(401, { 'Content-Type': 'application/json' }).end('{"ok":false,"motivo":"firma inválida"}');
      return;
    }

    let aviso;
    try {
      aviso = JSON.parse(cuerpoCrudo.toString('utf8'));
    } catch {
      res.writeHead(400, { 'Content-Type': 'application/json' }).end('{"ok":false,"motivo":"JSON inválido"}');
      return;
    }

    // 1) Respondé 2xx rápido.
    res.writeHead(200, { 'Content-Type': 'application/json' }).end('{"ok":true}');

    // 2) Procesá después, sin repetir.
    setImmediate(() => {
      const clave = claveDeDuplicado(aviso);
      if (clave && procesados.has(clave)) {
        console.log(`[aviso] repetido, no se procesa de nuevo: ${clave}`);
        return;
      }
      if (clave) procesados.add(clave);
      procesar(aviso);
    });
  });
});

servidor.listen(PUERTO, () => {
  console.log(`Esperando avisos en http://localhost:${PUERTO}${RUTA}`);
});

Entregas y buenas prácticas

Reglas de entrega
TemaCómo funciona hoy
ÉxitoCualquier respuesta 2xx. El cuerpo de tu respuesta se ignora.
ReintentosCada aviso se envía una vez. Si tu endpoint no responde 2xx, ese aviso no se vuelve a mandar. Los reintentos automáticos están en Próximamente.
RedireccionesSe siguen. Igual configurá la URL final para no depender de eso.
OrdenNo está garantizado entre eventos distintos. Un retiro puede recibir withdrawal.pending y después withdrawal.completed.
Identificador del avisoNo hay un ID de evento: deduplicá por event + data.id en los cobros y por event + data.coelsa_id en los retiros.
  1. Respondé 2xx en menos de 2 segundos. Guardá el aviso y procesalo en segundo plano. El aviso de un retiro se envía antes de que termine la respuesta de /transfer, así que tu demora también demora esa respuesta.
  2. Verificá la firma en cada aviso y descartá los que no coinciden.
  3. Deduplicá. Cobros: por event + data.id, porque un mismo cobro puede recibir transaction.in_review y después transaction.confirmed o transaction.refunded. Retiros: por event + data.coelsa_id cuando viene; si no viene, aplicá el cambio de estado solo si tu retiro sigue abierto.
  4. Conciliá todos los días con GET /external/transactions. Como hoy no hay reintentos, es la forma de recuperar un aviso que no te llegó (por ejemplo, si tu servidor estaba caído).
  5. Acreditá solo con transaction.confirmed, y siempre por data.monto, no por el monto esperado.
  6. Ignorá los eventos que no conocés y respondé 2xx igual: vamos a sumar eventos nuevos sin cambiar de versión.

Guías paso a paso

Las tres integraciones que hace todo operador.

Integrar cobros con depósito esperado

Los pasos 5 y 6 dependen de la acreditación automática

La acreditación automática de cobros y los avisos transaction.* están en Próximamente. Podés construir y probar todo el flujo ahora; hasta que te confirmemos que está activa en tu cuenta, ningún cobro se acredita ni te avisa.

  1. Configurá el webhook con tu URL y un secreto (PUT /external/webhook) y publicá el receptor.
  2. Pedí el CUIT del jugador una sola vez, en su registro o en su primer depósito. Tiene que ser el titular de la cuenta desde la que va a transferir.
  3. Cuando el jugador quiera depositar, creá un depósito esperado con el monto, el CUIT y el tipo de tráfico.
  4. Mostrale el CVU que te devolvió la API, el monto y un aviso claro: "Transferí desde una cuenta a tu nombre".
  5. Acreditá solo al recibir transaction.confirmed, siempre por data.monto.
  6. Con transaction.in_review, no acredites: mostrale al jugador que su depósito está en revisión. Cuando tu equipo lo resuelva en el panel, te llega transaction.confirmed o transaction.refunded con el mismo data.id.
  7. Conciliá cada noche con GET /external/transactions para recuperar cualquier aviso que no te haya llegado.
// 1) El jugador pide depositar $ 25.000 en tu sitio
const respuesta = await fetch('https://api.luckycobros.com.ar/api/v1/external/deposit/create', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.LUCKY_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    monto: 25000,
    contraparte_cuil_cuit: jugador.cuit,           // 11 dígitos, sin guiones
    traffic_type: jugador.yaDeposito ? 'std' : 'ftd',
  }),
});
const deposito = await respuesta.json(); // hoy solo llegan id, cvu y expires_at

// 2) Guardalo y mostrale el CVU al jugador
await db.depositos.insert({
  id: deposito.id, jugadorId: jugador.id, cuit: jugador.cuit,
  monto: 25000, cvu: deposito.cvu, estado: 'esperando',
});
mostrar('Transferí $ 25.000,00 al CVU ' + deposito.cvu + ' desde una cuenta a tu nombre.');
Node.js · al recibir el aviso
// 3) En tu receptor de avisos (después de verificar la firma y responder 200)
async function alRecibirCobro(aviso) {
  const cobro = aviso.data;
  const clave = aviso.event + ':' + cobro.id;           // un cobro puede pasar por in_review antes
  if (await db.avisos.existe(clave)) return;          // ya procesado

  await db.transaccion(async (tx) => {
    await tx.avisos.insert({ clave });

    if (aviso.event === 'transaction.in_review') {
      // Retenido: NO acredites. Mostrale al jugador que su depósito está en revisión.
      await tx.revision.insert({ cobroId: cobro.id, motivo: cobro.motivo, monto: cobro.monto });
      return;
    }
    if (aviso.event === 'transaction.refunded') {
      await tx.revision.cerrar(cobro.id, 'devuelto');      // se devolvió: no se acredita
      return;
    }
    if (aviso.event !== 'transaction.confirmed') return;   // evento que no conocés: ignoralo

    // transaction.confirmed: ya está en tu saldo. Si venía de revisión, cerrala.
    await tx.revision.cerrar(cobro.id, 'acreditado');
    // Misma regla que usa Lucky Cobros: el depósito pendiente más viejo de ese CUIT.
    // No des por vencidos tus depósitos por expires_at: para nosotros siguen pendientes.
    const deposito = await tx.depositos.masViejoPendiente({ cuit: cobro.contraparte_cuil_cuit });
    if (deposito) {
      await tx.depositos.actualizar(deposito.id, { estado: 'acreditado', recibido: cobro.monto });
      await tx.jugadores.acreditar(deposito.jugadorId, cobro.monto);   // siempre data.monto
    } else {
      await tx.pendientesDeAsignar.insert({ cobroId: cobro.id, cuit: cobro.contraparte_cuil_cuit, monto: cobro.monto });
    }
  });
}

Integrar retiros

  1. Generá una clave por retiro y guardala con el retiro en tu base antes de llamar a la API. Usá el ID del retiro: RT-000398.
  2. Validá el destino con /transfer/verify y, si viene el titular, mostralo para confirmar.
  3. Pedí el retiro con POST /external/transfer, esperando hasta 60 segundos.
  4. Interpretá la respuesta con la tabla de resultados: done, pending, error, duplicate, 400, 409 o 500.
  5. Escuchá los avisos withdrawal.completed, withdrawal.pending y withdrawal.failed para cerrar los que quedaron abiertos.
  6. Ante cualquier duda, reintentá con la misma clave, nunca con una nueva. Si a los 10 minutos sigue sin confirmarse, escribinos con la clave.
Node.js · flujo completo de un retiro
// Flujo recomendado para un retiro (pseudocódigo en Node.js)
async function retirar(jugador, monto, cbu) {
  // 1) Validá el destino (opcional) y mostrale el titular a quien pide el retiro
  // 2) Creá el retiro en TU base primero, con su ID estable
  const retiro = await db.retiros.insert({ id: 'RT-000398', jugadorId: jugador.id, cbu, monto, estado: 'pedido' });

  // 3) Pedilo a la API (ver pedirRetiro() en la referencia de POST /external/transfer)
  const resultado = await pedirRetiro({ id: retiro.id, cbu, monto, concepto: 'Retiro de premios' });
  // El aviso suele llegar ANTES que esta respuesta: guardá el resultado solo si nadie cerró el retiro
  await db.retiros.actualizarSiEstado(retiro.id, 'pedido', { estado: resultado });

  // 4) Si quedó 'en_duda' o 'consultar', NO generes otro retiro: reintentá más tarde con el MISMO id
  //    o esperá withdrawal.completed. Si pasa a requiere_revision no llega aviso: escribinos a soporte.
}

// 5) En tu receptor de avisos
async function alRecibirRetiro(aviso) {
  const d = aviso.data;
  // Por cbu + monto entre los abiertos ('pedido', 'pendiente' o 'en_duda'): todavía no tenés el coelsa_id
  const retiro = await db.retiros.abierto({ cbu: d.cbu, monto: d.monto });
  if (!retiro) return;   // ya cerrado: es un aviso repetido

  if (aviso.event === 'withdrawal.completed') await db.retiros.actualizar(retiro.id, { estado: 'confirmado', coelsaId: d.coelsa_id });
  if (aviso.event === 'withdrawal.failed') await db.retiros.actualizar(retiro.id, { estado: 'fallido' }); // la plata volvió
  if (aviso.event === 'withdrawal.pending' && d.status === 'unknown') await db.retiros.actualizar(retiro.id, { estado: 'en_duda' });
}

Probar webhooks localmente

Primero probá la firma en tu máquina, sin plata ni API de por medio. Después, con un túnel, recibí un aviso real.

Terminal · prueba sin conexión
# 1) Levantá el receptor de ejemplo con un secreto de prueba
export WEBHOOK_SECRET="whsec_prueba_123"
node ejemplos/verificar-firma.js          # o: php -S localhost:3000 ejemplos/verificar-firma.php
                                          # o: python ejemplos/verificar-firma.py

# 2) En otra terminal, firmá un aviso de prueba y mandalo
BODY='{"event":"transaction.confirmed","data":{"id":"0f5c1e7a-3b2d-4e8f-9a6c-1d2e3f4a5b6c","codigo_coelsa":"MP36D4CPEC9G","monto":25000,"contraparte_nombre":"MICAELA SOSA","contraparte_cuil_cuit":"27402187354","status":"confirmada"}}'
FIRMA=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.*= //')

curl -i "http://localhost:3000/webhooks/cobros" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Event: transaction.confirmed" \
  -H "X-Webhook-Timestamp: 2026-10-28T17:03:11.482Z" \
  -H "X-Webhook-Signature: sha256=$FIRMA" \
  --data-raw "$BODY"
# → HTTP/1.1 200 OK  {"ok":true}
# Mandalo dos veces: la segunda vez el receptor avisa que está repetido.
# Cambiá un dígito del monto en BODY sin recalcular la firma: tiene que responder 401.
Terminal · aviso real con un túnel
# 3) Exponé tu puerto con un túnel HTTPS (por ejemplo ngrok o cloudflared)
ngrok http 3000
# → https://a1b2-203-0-113-24.ngrok-free.app

# 4) Apuntá el webhook a esa URL con el MISMO secreto que usa tu receptor
curl -X PUT "https://api.luckycobros.com.ar/api/v1/external/webhook" \
  -H "X-API-Key: $LUCKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://a1b2-203-0-113-24.ngrok-free.app/webhooks/cobros",
    "webhook_secret": "whsec_prueba_123"
  }'

# 5) Generá un evento real: un retiro de $ 1,00 a una cuenta propia externa
#    (por ejemplo, la cuenta bancaria de tu empresa, no una de tus cuentas CVU de cobro)
#    y mirá llegar withdrawal.completed en la consola del receptor.
# 6) Al terminar, volvé a configurar tu URL y tu secreto de producción.

Si usás Windows

En PowerShell, definí el secreto con $env:WEBHOOK_SECRET="whsec_prueba_123". Para firmar el aviso de prueba, usá Git Bash o WSL, que traen openssl.

Próximamente

Lo que estamos construyendo. Nada de esto está disponible todavía: no lo uses en tu integración.

  • PróximamenteAcreditación automática de cobrosCada transferencia que entra a tus cuentas CVU se concilia por CUIT con tu depósito esperado: si coincide, se acredita y te llega transaction.confirmed; si no, queda retenida y te llega transaction.in_review. Los depósitos esperados ya se pueden crear.
  • PróximamenteRevisión de cobrosAcreditar o devolver desde el panel un cobro retenido, con doble control. Al resolverlo te llega transaction.confirmed o transaction.refunded.
  • PróximamenteEntorno de pruebasUna URL aparte con plata ficticia para probar cobros, retiros y avisos de punta a punta.
  • PróximamenteRotación por tope mensualCada cuenta CVU deja de asignarse al acercarse a su tope del mes y el reparto sigue entre las demás. Si todas tus cuentas CVU llegaron al tope del mes, GET /external/cbu y POST /external/deposit/create responden 400 con code ALL_ACCOUNTS_AT_LIMIT y no se crea el depósito: mostrale al jugador otro medio de pago o pedí una cuenta CVU nueva. Te avisamos antes, cuando una cuenta llega al 90 %.
  • PróximamenteVencimiento de depósitos esperadosUn depósito pendiente vence en expires_at y deja de conciliar cobros. Un cobro que entra después de expires_at no se acredita solo: te llega transaction.in_review con motivo sin_deposito, y tu equipo lo acredita o lo devuelve desde A revisar. La vigencia por defecto se va a poder fijar en Integración.
  • PróximamenteDepósito esperado completoLa respuesta de POST /external/deposit/create va a sumar alias, el titular de la cuenta CVU, monto_esperado, traffic_type y status (pendiente), para que le muestres al jugador a quién le transfiere. Además, un campo opcional jugador_nombre que se ve en el panel (A revisar y Transacciones) y ayuda a tu equipo a resolver un cobro retenido. No cambia cómo se concilia.
  • PróximamenteCobrar en una cuenta CVU puntualUn campo opcional cuenta_id en POST /external/deposit/create para que el jugador transfiera a una cuenta CVU puntual (por ejemplo, la de su sucursal). Si no lo mandás, sigue la rotación automática. Si la cuenta no es tuya responde 404 (ACCOUNT_NOT_FOUND); si está pausada o llegó al tope del mes, 400 (ACCOUNT_NOT_OPERATIVE) y no se crea el depósito.
  • PróximamenteReintentos de avisosHasta 6 intentos con espera creciente y un ID único por evento para deduplicar.
  • PróximamenteFirma con horaLa hora de envío dentro de la firma, para rechazar avisos viejos repetidos.
  • PróximamenteAvisos más completos

    Todos los campos se suman a los actuales, sin quitar ninguno.

    • Cobros (transaction.*): deposito_id, monto_esperado, traffic_type, cuenta_cvu y cuenta_alias de la cuenta CVU que recibió la plata, y la fecha de la transferencia. motivo sigue como ya está definido.
    • Retiros (withdrawal.*): el id del retiro, tu idempotency_key, la fecha de pedido y la de cierre, el titular de destino y la cuenta CVU de origen. En withdrawal.failed, además, el motivo (code, por ejemplo DESTINATION_REJECTED) con un texto legible.
    • Origen: campo origen con api o panel (y la sucursal, si salió del panel), para que tu sistema no le descuente al jugador un retiro manual que hizo tu equipo, o lo concilie aparte.
    • Rechazo posterior: si un retiro aceptado (withdrawal.pending, status pending) después lo rechaza la entidad de destino, la plata vuelve a tu saldo y te llega withdrawal.failed con el mismo id y motivo DESTINATION_REJECTED.

    Con deposito_id e idempotency_key, los receptores de ejemplo van a cruzar cada aviso directo con tu pedido, en vez de buscar el depósito pendiente más viejo de ese CUIT o el retiro por CBU y monto.

  • PróximamenteURL de aviso por sucursalUna URL de webhook propia para cada sucursal: los avisos de los cobros y retiros de sus cuentas CVU van a esa URL en vez de la de tu empresa. Una sucursal sin URL propia sigue usando la general. Se configura en Integración › Webhook.
  • PróximamentePausar avisosPausar el webhook desde Integración sin perder avisos: mientras está pausado no se envía nada, y lo que quedó sin enviar se puede reenviar durante 30 días cuando lo reactivás. También vas a poder quitar la URL.
  • PróximamenteSecreto generado por la plataformaCambio de seguridad: el secreto de firma se va a generar en Integración › Webhook y se muestra una sola vez. GET /external/webhook lo va a devolver enmascarado y PUT /external/webhook solo va a cambiar la URL, que tiene que ser https y pública. Mientras tanto, generalo vos como indica Configuración.
  • PróximamenteComisiones y saldo netoGET /external/balance va a sumar comisiones_pendientes por cuenta CVU y en total, con saldo_neto = saldo_real − comisiones_pendientes. Los retiros se validan contra el saldo neto: es lo que podés retirar. Los retiros no tienen costo.
  • PróximamenteConsulta de retirosGET /external/transfers/{id} y GET /external/transfers?idempotency_key=RT-000398, con status, monto, destino, titular, cuenta CVU de origen, fecha de pedido y de cierre, y código de la transferencia. Además, POST /external/transfer va a devolver el id del retiro y tu idempotency_key. Cuando salga, en Retiros en duda vas a consultar el retiro así, en vez de buscarlo en /transactions.
  • PróximamenteElegir la cuenta de origen de un retiroUn campo opcional cuenta_id en POST /external/transfer para que el retiro salga de una cuenta CVU puntual (por ejemplo, la de una sucursal). Si no lo mandás, sale de la de mayor saldo, como hoy. Si la cuenta no es tuya responde 404 ACCOUNT_NOT_FOUND; si no está operativa, 400 ACCOUNT_NOT_OPERATIVE.
  • PróximamenteRetiros repartidosUn retiro que toma saldo de varias cuentas CVU cuando ninguna alcanza sola. Usás una sola idempotency_key para el total; la respuesta trae el reparto (cuenta CVU y monto de cada parte) y lo consultás con GET /external/transfers/{id}, con el estado del total y de cada parte. withdrawal.completed sale cuando se completó el total.
  • PróximamenteRetiro solo al titularCampos opcionales titular_cuit y titular_nombre en POST /external/transfer. Se guardan con el retiro y vuelven en la consulta del retiro y en los avisos withdrawal.*. Cuando la validación del destino confirme el titular (ver Retiros a un alias), si el CUIT del destino no coincide con el que mandaste, el retiro no sale y responde 422 DESTINATION_HOLDER_MISMATCH, sin mover plata. Así un premio se paga solo a la cuenta del propio jugador.
  • PróximamenteRetiros a un aliasMandar el destino como alias, además de CBU/CVU de 22 dígitos, en POST /external/transfer y POST /external/transfer/verify. La validación también te dice la entidad de destino y si es CBU o CVU, y confirma que la cuenta existe (no solo el formato). Se va a poder usar con una key de solo lectura.
  • PróximamenteRetiros a tus propias cuentas CVUUn retiro cuyo destino es uno de tus CVU de cobro va a responder 400 SELF_TRANSFER («el destino es una de tus cuentas CVU de cobro»), sin mover plata. Para tus pruebas de hoy, usá una cuenta externa (ver Cómo probar mientras tanto).
  • PróximamenteCuentas CVU por APIGET /external/accounts (filtro por estado, paginado) y GET /external/accounts/{id}, con CVU, alias, nombre interno, titular, saldo, tope del mes, acumulado del mes, disponible hasta el tope y estado (operativa, pausada o al tope).
  • PróximamenteValidación de datosRespuestas 400 que indican qué campo corregir en un retiro o en un depósito esperado.
  • PróximamenteFiltros y resúmenes
    • Filtros de GET /external/transactions: tipo (cobro o pago), status, from y to, cuenta_id y q para buscar por CUIT o CUIL, nombre, CBU, código de la transferencia o tu idempotency_key, con los mismos nombres que los avisos.
    • Tope de paginación: limit máximo 100 (si pedís más, se usa 100) y 400 INVALID_PAGINATION para valores no enteros o negativos.
    • Resumen del período: GET /external/transactions/summary?from&to&cuenta_id → cobros {total, cantidad}, retiros {total, cantidad}, comisiones {total} y neto.
    • Movimiento por día: GET /external/transactions/daily?from&to, con los días sin movimiento en cero (por defecto, los últimos 30 días).
  • PróximamenteTransacciones v2

    Las filas de GET /external/transactions en snake_case y con los mismos nombres que los avisos: id, codigo_coelsa, referencia, contraparte_nombre, contraparte_cuil_cuit, contraparte_cbu, monto, monto_esperado, traffic_type y status, más comision, concepto, cuenta {cvu, alias}, origen (api o panel), motivo y fecha, sin identificadores internos.

    Se va a publicar como /api/v2 o con un parámetro de formato, sin romper a quien ya integró el formato actual.

  • PróximamenteCódigos de error y límites

    Límites de pedidos por key:

    Límites de pedidos previstos
    PedidosLímite
    Consultas1.000 pedidos por minuto por key
    Retiros y depósitos esperados100 pedidos por minuto (POST /external/transfer y POST /external/deposit/create)

    Cada respuesta va a traer X-RateLimit-Limit y X-RateLimit-Remaining. Si te pasás, recibís 429 con code RATE_LIMITED y Retry-After: esperá lo que indica Retry-After; en un retiro, reintentá con la misma idempotency_key.

    Códigos de error: un campo code que se suma a los cuerpos actuales, sin romper la v1 (message, statusCode y error siguen igual). Hoy el único que existe es IP_NOT_WHITELISTED.

    Códigos de error previstos
    codeHTTPQué hacer
    MISSING_API_KEY401Mandá el encabezado X-API-Key.
    INVALID_API_KEY401La key no existe o fue revocada: usá una vigente.
    IP_NOT_WHITELISTED403Ya existe hoy. Pedí desde una IP habilitada o sumala a la lista de la key.
    INSUFFICIENT_PERMISSIONS403La key no tiene el permiso del endpoint (read o write).
    INVALID_AMOUNT400El monto tiene que ser un número mayor a cero.
    INVALID_DESTINATION400El destino no es un CBU/CVU de 22 dígitos ni un alias válido. Corregilo.
    INVALID_CUIT400El CUIT o CUIL tiene que tener 11 dígitos, sin guiones.
    INVALID_PAGINATION400page y limit tienen que ser enteros positivos (limit hasta 100).
    MISSING_IDEMPOTENCY_KEY400Todo retiro lleva idempotency_key: generala y guardala antes de llamar.
    DUPLICATE_REQUEST409Ya existe un retiro con esa idempotency_key. Consultá el original; no lo repitas con otra clave.
    WITHDRAWAL_IN_PROGRESS409Hay otro retiro saliendo desde esa cuenta CVU. Esperá unos segundos y reintentá con la misma idempotency_key.
    NO_ACTIVE_ACCOUNTS400No tenés cuentas CVU operativas: reactivá una o pedí una nueva.
    ALL_ACCOUNTS_AT_LIMIT400Todas tus cuentas CVU llegaron al tope del mes: ofrecele al jugador otro medio de pago o pedí una cuenta CVU nueva.
    ACCOUNT_NOT_FOUND404El cuenta_id no es de una cuenta CVU tuya. Revisalo.
    ACCOUNT_NOT_OPERATIVE400La cuenta CVU elegida está pausada o al tope: elegí otra o no mandes cuenta_id.
    INSUFFICIENT_BALANCE400No alcanza el saldo neto para ese retiro: retirá menos o esperá nuevos cobros.
    SELF_TRANSFER400El destino es una de tus cuentas CVU de cobro: elegí una cuenta externa.
    DESTINATION_HOLDER_MISMATCH422El CUIT del titular del destino no coincide con titular_cuit. No se movió plata: revisá el destino con el jugador.
    RATE_LIMITED429Esperá lo que indica Retry-After; en un retiro, reintentá con la misma idempotency_key.
    DESTINATION_REJECTED422La entidad de destino rechazó el retiro y la plata volvió a tu saldo. Revisá el destino antes de pedirlo de nuevo.
    OUTCOME_UNKNOWN500 · 504Retiro en duda: consultalo antes de reintentar y, si reintentás, usá la misma idempotency_key. Ver Retiros en duda.
    INTERNAL_ERROR500Error de nuestro lado. En consultas, reintentá con espera (1 s, 5 s, 30 s).
  • PróximamenteIPs por rangoCargar un rango en notación CIDR (por ejemplo 203.0.113.0/28) además de IPs sueltas, para servidores que salen por más de una IP. Mientras tanto, cargá cada IP exacta (ver IPs permitidas).
  • PróximamenteSesiones del panelRecordar un equipo de confianza para no pedir el código en cada ingreso, y un código por correo como respaldo de la app de autenticación.
  • PróximamenteDocumentación en inglésLa misma referencia, guías y ejemplos en inglés, para equipos técnicos de otros países.

Cuando algo de esta lista salga, lo vas a ver en el changelog. Los cambios que rompan compatibilidad van en una versión nueva de la URL (/api/v2), con aviso previo.

Changelog

  1. v1.0 · primera versión pública

    28/10/2026

    • Autenticación por X-API-Key con permisos read y write e IPs permitidas.
    • Saldos y cuentas CVU: GET /external/balance y GET /external/cbu con rotación de CVU.
    • Cobros: POST /external/deposit/create con tipo de tráfico FTD/STD. La acreditación automática y la conciliación por CUIT están en Próximamente.
    • Retiros: POST /external/transfer con idempotency_key y POST /external/transfer/verify.
    • Movimientos: GET /external/transactions paginado.
    • Webhooks firmados con HMAC-SHA256: 3 eventos de retiro. Los 3 de cobro (transaction.confirmed, transaction.in_review y transaction.refunded) ya están definidos y se envían con la acreditación automática. Configuración con GET y PUT /external/webhook.
    • Especificación OpenAPI 3.1, colección Postman y receptores de ejemplo en Node.js, PHP y Python.
Lucky Cobros · API v1.0 · actualizada el 28/10/2026 Volver arriba