Retiros
Retiros: de fichas a USDT/USDC
Paga a tus usuarios en USDT o USDC desde una caja que tú fondeas: el camino inverso a un depósito.
Los depósitos llevan dinero de tus usuarios a tu empresa. Los retiros hacen lo contrario: tu empresa paga a un usuario en USDT o USDC, a la billetera que él elija. El caso típico: un usuario tiene 500 fichas en tu plataforma (1 ficha = 1 USD) y pide cambiarlas por USDC. Tú descuentas las fichas en tu sistema y llamas a la API; en unos segundos el usuario tiene 500 USDC en su billetera.
- 1
Fondeas tu caja
Envías USDT o USDC a tu dirección de fondeo, desde cualquier billetera o exchange. Es como un depósito, pero al revés: ese dinero queda en tu caja de retiros.
- 2
El usuario pide su retiro
En tu aplicación elige el monto y pega su billetera. Tú descuentas las fichas.
- 3
Llamas a la API
Con Crear retiro envías el token, el monto, la billetera y tu
external_id. El saldo queda reservado al instante. - 4
Recibes el resultado
El webhook
payout.completedconfirma el pago con su transacción. Si llegapayout.failed, no se movió dinero: devuelve las fichas.
Los retiros se activan por token
Hasta que se configuran los límites de un token, sus retiros están apagados y la API responde 422 payouts_disabled. Pide a la plataforma que los fije (o fíjalos tú si tienes billetera administradora propia: ver Límites).
Cómo funciona por dentro#
- Hay un solo contrato de retiros para toda la pasarela, con una caja separada por empresa. El saldo de una empresa no se mezcla con el de otra: el contrato lleva la cuenta de cada una.
- La plataforma solo firma la transacción del pago. El contrato comprueba, en cada retiro, el saldo de tu caja, tus límites y que la comisión no supere la configurada.
- Tu dinero solo puede salir de tu caja de dos formas: un retiro a un usuario (dentro de tus límites) o una devolución a tu tesorería. No existe ninguna otra salida.
- Cada retiro lleva un id en la blockchain que el contrato no acepta dos veces: ni un reintento ni un fallo de red pueden pagar el mismo retiro dos veces.
Fondear la caja#
Consulta tu dirección de fondeo con Ver saldo de retiros (funding_address) o en el panel, en Retiros. Envía ahí USDT o USDC en la red Arbitrum. No hace falta avisar a nadie: el servidor revisa las direcciones de fondeo cada pocos segundos, pasa el dinero a tu caja y te envía el webhook balance.funded.
curl https://api.pasarela.hi-g.io/v1/payouts/balance -H "x-api-key: sk_test_4f9c…"- Mientras el fondeo se acredita, aparece en
funding_pendingy ya cuenta como disponible. - Se recoge a partir de 1 USD por token. Montos menores se quedan en la dirección hasta que se junte 1 USD: así nadie puede hacer gastar gas enviando migajas.
- La dirección de fondeo es permanente y solo sirve para tu empresa. Si por error llega ETH, va entero a tu tesorería.
- Si prefieres fondear desde un contrato o una billetera propia, también puedes llamar a
fund(merchantId, token, amount)del contrato tras unapprove.
Pagar a un usuario#
- 1
Descuenta las fichas
En tu base de datos, antes de llamar a la API. Guarda la operación con un id propio.
- 2
Crea el retiro
POST /v1/payoutscontoken,amount(las fichas, 1 = 1 USD),to(la billetera del usuario) yexternal_id(tu id de la operación). Respuesta201: aceptado y reservado. - 3
Si la llamada falla por red, repítela
Con el mismo
external_id. Si ya se había creado, recibes el mismo retiro (200,created: false). - 4
Si responde 4xx, no se movió nada
Devuelve las fichas al usuario y muéstrale el motivo (
error). El campocodete dice cuál fue. - 5
Espera el webhook
payout.completed: pagado, contx_hash.payout.failed: no se pagó y el saldo vuelve a estar disponible; devuelve las fichas.
1# 1. Cuánto recibiría y cuánto sale de tu caja (opcional)2curl -X POST https://api.pasarela.hi-g.io/v1/payouts/quote \3 -H "x-api-key: sk_test_4f9c…" \4 -H "content-type: application/json" \5 -d '{"token": "USDT", "amount": "500"}'6 7# 2. Pagar. external_id es tu id de la operación: repetirlo nunca paga dos veces8curl -X POST https://api.pasarela.hi-g.io/v1/payouts \9 -H "x-api-key: sk_test_4f9c…" \10 -H "content-type: application/json" \11 -d '{12 "token": "USDT",13 "amount": "500",14 "to": "0xF389A660Fa404c39E8f346C46E569fF41b8d3d68",15 "external_id": "retiro-000981",16 "customer_id": "jugador-77",17 "metadata": {"fichas": 500}18 }'19 20# 3. Consultar por tu propio id21curl https://api.pasarela.hi-g.io/v1/payouts/ext:retiro-000981 -H "x-api-key: sk_test_4f9c…"1{2 "payout": {3 "id": "6b1f0c3e-1d7a-4a39-9a7e-2f5c8d0b4e11",4 "external_id": "retiro-000981",5 "customer_ref": "jugador-77",6 "status": "pending",7 "chain_id": 421614,8 "token": "USDT",9 "token_address": "0xdc190461C20DE36fFB305b0190985B4023D7D183",10 "decimals": 6,11 "to": "0xF389A660Fa404c39E8f346C46E569fF41b8d3d68",12 "requested": "500",13 "amount": "500",14 "platform_fee": "5",15 "gas_fee": "0.008419",16 "debit": "505.008419",17 "gas_paid_by": "merchant",18 "tx_hash": null,19 "error": null,20 "metadata": {21 "fichas": 50022 },23 "created_at": "2026-10-14T13:20:05.114Z",24 "sent_at": null,25 "completed_at": null,26 "failed_at": null27 },28 "created": true29}Solo billeteras Hig#
Con solo billeteras Hig (activo por defecto), tus usuarios solo pueden cobrar en la billetera de su app Hig (Android e iOS). Antes de aceptar cada retiro, la pasarela comprueba en el registro de usuarios de Hig que to es la billetera de un usuario de Hig. Si no lo es, el retiro se rechaza con 422 not_hig_wallet y no se mueve nada.
- Cuando el usuario escriba su dirección, llama a Comprobar billetera. Si responde
allowed: false, pídele que descargue Hig (download_url) y te pase la dirección de su billetera Hig. - Haz esa comprobación antes de descontar las fichas: así el usuario no tiene que esperar a que se las devuelvas.
- Si el registro de Hig no responde, el retiro se rechaza con
503 hig_check_unavailable: no se adivina. Devuelve las fichas y repítelo en unos minutos. - Lo activa o lo apaga la plataforma para cada empresa. En tu panel, en Retiros, verás si está activo.
Estados#
| Estado | Qué significa | Qué hacer |
|---|---|---|
Estadopending | Qué significaAceptado y en cola. El saldo ya está reservado. | Qué hacerMostrar "procesando". |
Estadosent | Qué significaLa transacción está enviada a la red. | Qué hacerEsperar (normalmente un par de segundos). |
Estadocompleted | Qué significaEl usuario ya tiene el dinero. Es definitivo. | Qué hacerMarcar pagado y guardar tx_hash. |
Estadofailed | Qué significaNo se pagó nada. El saldo reservado vuelve a la caja. | Qué hacerDevolver las fichas al usuario. |
Un retiro va de pending a completed en unos 3 a 5 segundos. Si una transacción se pierde en la red, el servidor mira en el contrato si el retiro ya se pagó: si sí, lo cierra como completed; si no, lo vuelve a enviar con el mismo id, así que nunca se paga dos veces.
Quién paga comisión y gas#
La plataforma puede cobrar una comisión por retiro (payout_fee_bps, en puntos básicos) y el pago en sí cuesta gas. Quién lo paga lo define gas_paid_by de tu configuración. Ejemplo: el usuario cambia 500 fichas, comisión 1 %, gas 0.008419 USDT.
gas_paid_by | El usuario recibe | Sale de tu caja | Comisión | Gas |
|---|---|---|---|---|
gas_paid_bymerchant (tu empresa) | El usuario recibe500 | Sale de tu caja505.008419 | Comisión5 | Gas0.008419 |
gas_paid_bycustomer (el usuario) | El usuario recibe494.991581 | Sale de tu caja500 | Comisión5 | Gas0.008419 |
gas_paid_byplatform | El usuario recibe500 | Sale de tu caja505 | Comisión5 | Gas0 (lo absorbe la plataforma) |
comisión = ⌊ pedido × fee_bps
/ 10000 ⌋
merchant
recibe = pedido
sale = pedido + comisión + gas
customer
recibe = pedido − comisión − gas
sale = pedido
platform (gas = 0)
recibe = pedido
sale = pedido + comisión- Comisión y gas van a la billetera de la plataforma en la misma transacción del pago.
- El gas se estima con margen al crear el retiro y queda fijo en ese retiro (
gas_fee). Nunca supera el tope por token del contrato (0.5 USD). - El contrato tiene guardada tu comisión de retiros y rechaza cualquier pago que cobre más: el servidor no puede subirla por su cuenta.
- Si con
customerla comisión y el gas se comen el monto, la API responde422 amount_too_small. - Para saber los números antes de pagar, usa Cotizar retiro.
Límites#
Cada token tiene dos límites, guardados en el contrato. Protegen tu caja: aunque alguien se hiciera con la clave del relayer de la plataforma, no podría sacar más que eso por día.
| Límite | Qué controla |
|---|---|
Límitemax_per_payout | Qué controlaLo máximo que puede recibir un usuario en un retiro. En 0, los retiros de ese token están apagados. |
Límitedaily_limit | Qué controlaLo máximo que pueden recibir todos tus usuarios juntos en un día (00:00 a 23:59 UTC). Se renueva solo cada día. |
- Si la plataforma administra tu empresa (no pusiste billetera administradora propia), pídele que los fije o los cambie. Ella usa Fijar límites de retiro.
- Si tienes billetera administradora propia, la plataforma no puede tocarlos: los fijas tú llamando a
setLimits(merchantId, token, maxPerPayout, dailyLimit)del contrato de retiros desde esa billetera. Los montos van en la unidad mínima del token. remaining_todayen el saldo ya descuenta los retiros aceptados que aún no salieron.
Idempotencia#
- Cada retiro necesita una clave: el campo
external_ido la cabeceraIdempotency-Key. Sin ninguna de las dos,400. - Mismo
external_idcon los mismos datos: devuelve el retiro existente (200,created: false) sin crear otro. - Mismo
external_idcon otro token, monto o destino:409 idempotency_conflict. - Además, en la blockchain cada retiro tiene su propio id y el contrato lo rechaza si ya se pagó (
AlreadyPaid). Son dos capas: tu clave evita crear dos retiros y el contrato evita pagar dos veces el mismo.
Seguridad y responsabilidades#
Una transferencia en blockchain no se puede deshacer
La billetera de destino la elige tu usuario y es responsabilidad de cada cliente cuidar su seguridad y escribirla bien. Si envía a una dirección equivocada, nadie (ni tú ni la plataforma) puede recuperar ese dinero. Pídele que la confirme y, si puedes, que pegue la dirección en lugar de escribirla.
- Se rechazan como destino (
422 invalid_recipient): la dirección cero, tu dirección de fondeo, los contratos de la pasarela y cualquier dirección de depósito de la pasarela (el dinero volvería a entrar como un depósito). - Se aceptan billeteras normales (MetaMask, Trust, Rabby), billeteras de exchanges y billeteras inteligentes (Safe y similares), siempre en la red Arbitrum.
- Protege tu clave de API: con ella se pueden crear retiros dentro de tus límites. Por eso conviene fijar límites ajustados a tu operación y fondear la caja con lo que necesitas, no con toda tu tesorería.
Devolver saldo a tu tesorería#
Lo que sobre en la caja puede volver a tu tesorería cuando quieras con Devolver saldo a la tesorería. El contrato solo permite enviarlo a tu tesorería, nunca a otro destino. No se puede devolver el saldo reservado por retiros en curso.
1curl -X POST https://api.pasarela.hi-g.io/v1/payouts/return \2 -H "x-api-key: sk_test_4f9c…" \3 -H "content-type: application/json" \4 -d '{"token": "USDT", "amount": "200"}'Si la plataforma desaparece#
Tu saldo no depende de que la plataforma siga operando. Si tienes billetera administradora propia, llamas a withdraw(merchantId, token, amount) del contrato de retiros desde esa billetera y el dinero va a tu tesorería. Si la plataforma administra tu empresa, también puede devolverlo por ti, y solo a tu tesorería. Ver Sin custodia.
Errores#
Los rechazos de retiros traen, además de error, un campo code estable para que tu código decida sin leer el texto: { "error": "El máximo por retiro es 600 USDT", "code": "above_max_per_payout" }.
code | HTTP | Cuándo |
|---|---|---|
codeinsufficient_balance | HTTP422 | CuándoEl total que saldría de la caja supera lo disponible (available). Fondea o espera a que se acredite el fondeo. |
codepayouts_disabled | HTTP422 | CuándoLos retiros de ese token están apagados: faltan los límites. |
codeabove_max_per_payout | HTTP422 | CuándoLo que recibiría el usuario supera max_per_payout. |
codeabove_daily_limit | HTTP422 | CuándoNo queda tanto límite diario hoy (remaining_today). |
codebelow_minimum | HTTP422 | CuándoEl monto es menor que el mínimo por retiro (min_payout, 1 USD por defecto). |
codeinvalid_recipient | HTTP422 | CuándoDestino no permitido (ver Seguridad). |
codeamount_too_small | HTTP422 | CuándoCon gas_paid_by: customer, la comisión y el gas se comen el monto. |
codenot_hig_wallet | HTTP422 | CuándoCon solo billeteras Hig, to no es la billetera de ningún usuario de la app Hig. |
codeidempotency_conflict | HTTP409 | CuándoYa hay un retiro con ese external_id y datos distintos. |
codehig_check_unavailable | HTTP503 | CuándoNo se pudo consultar el registro de Hig. No se movió nada: repítelo en unos minutos. |
Si el retiro se aceptó pero el contrato lo rechaza al pagarlo (por ejemplo, porque apagaste tus límites entre medias), llega payout.failed con error.code:
error.code en payout.failed | Motivo |
|---|---|
error.code en payout.failedinsufficient_balance | MotivoLa caja ya no tenía saldo suficiente al pagar. |
error.code en payout.failedpayouts_disabled | MotivoLos límites del token se pusieron en 0 después de aceptar el retiro. |
error.code en payout.failedabove_max_per_payout / above_daily_limit | MotivoLos límites bajaron después de aceptar el retiro. |
error.code en payout.failedinvalid_recipient | MotivoEl contrato rechazó el destino. |
error.code en payout.failedfee_too_high / gas_above_cap | MotivoLa comisión o el gas superan lo que permite el contrato. |
error.code en payout.failedunknown_merchant / relayer_not_authorized | MotivoProblema de configuración de la plataforma. |
error.code en payout.failedsend_failed | MotivoLa red no aceptó la transacción tras varios intentos. |
Webhooks de retiros#
Llegan a la misma webhook_url que los depósitos, firmados igual. Ver Webhooks.
1{2 "id": "d5b2f8e3-6c4a-4f9b-8d1e-2a3b4c5d6e7f",3 "type": "payout.completed",4 "created_at": "2026-10-14T13:20:08.951Z",5 "data": {6 "payout": {7 "id": "6b1f0c3e-1d7a-4a39-9a7e-2f5c8d0b4e11",8 "external_id": "retiro-000981",9 "customer_ref": "jugador-77",10 "status": "completed",11 "chain_id": 421614,12 "token": "USDT",13 "token_address": "0xdc190461C20DE36fFB305b0190985B4023D7D183",14 "decimals": 6,15 "to": "0xF389A660Fa404c39E8f346C46E569fF41b8d3d68",16 "requested": "500",17 "amount": "500",18 "platform_fee": "5",19 "gas_fee": "0.008419",20 "debit": "505.008419",21 "gas_paid_by": "merchant",22 "tx_hash": "0x9d2c41b07e3f5a6c8e1b0d4f7a2c9e6b3d8f1a0c5e7b2d4f6a8c0e1b3d5f7a9c",23 "error": null,24 "metadata": {25 "fichas": 50026 },27 "created_at": "2026-10-14T13:20:05.114Z",28 "sent_at": "2026-10-14T13:20:06.302Z",29 "completed_at": "2026-10-14T13:20:08.947Z",30 "failed_at": null31 }32 }33}