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. 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. 2

    El usuario pide su retiro

    En tu aplicación elige el monto y pega su billetera. Tú descuentas las fichas.

  3. 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. 4

    Recibes el resultado

    El webhook payout.completed confirma el pago con su transacción. Si llega payout.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_pending y 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 un approve.

Pagar a un usuario#

  1. 1

    Descuenta las fichas

    En tu base de datos, antes de llamar a la API. Guarda la operación con un id propio.

  2. 2

    Crea el retiro

    POST /v1/payouts con token, amount (las fichas, 1 = 1 USD), to (la billetera del usuario) y external_id (tu id de la operación). Respuesta 201: aceptado y reservado.

  3. 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. 4

    Si responde 4xx, no se movió nada

    Devuelve las fichas al usuario y muéstrale el motivo (error). El campo code te dice cuál fue.

  5. 5

    Espera el webhook

    payout.completed: pagado, con tx_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#

EstadoQué significaQué hacer
EstadopendingQué significaAceptado y en cola. El saldo ya está reservado.Qué hacerMostrar "procesando".
EstadosentQué significaLa transacción está enviada a la red.Qué hacerEsperar (normalmente un par de segundos).
EstadocompletedQué significaEl usuario ya tiene el dinero. Es definitivo.Qué hacerMarcar pagado y guardar tx_hash.
EstadofailedQué 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_byEl usuario recibeSale de tu cajaComisiónGas
gas_paid_bymerchant (tu empresa)El usuario recibe500Sale de tu caja505.008419Comisión5Gas0.008419
gas_paid_bycustomer (el usuario)El usuario recibe494.991581Sale de tu caja500Comisión5Gas0.008419
gas_paid_byplatformEl usuario recibe500Sale de tu caja505Comisión5Gas0 (lo absorbe la plataforma)
Fórmula
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 customer la comisión y el gas se comen el monto, la API responde 422 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ímiteQué controla
Límitemax_per_payoutQué controlaLo máximo que puede recibir un usuario en un retiro. En 0, los retiros de ese token están apagados.
Límitedaily_limitQué 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_today en el saldo ya descuenta los retiros aceptados que aún no salieron.

Idempotencia#

  • Cada retiro necesita una clave: el campo external_id o la cabecera Idempotency-Key. Sin ninguna de las dos, 400.
  • Mismo external_id con los mismos datos: devuelve el retiro existente (200, created: false) sin crear otro.
  • Mismo external_id con 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.

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

codeHTTPCuándo
codeinsufficient_balanceHTTP422CuándoEl total que saldría de la caja supera lo disponible (available). Fondea o espera a que se acredite el fondeo.
codepayouts_disabledHTTP422CuándoLos retiros de ese token están apagados: faltan los límites.
codeabove_max_per_payoutHTTP422CuándoLo que recibiría el usuario supera max_per_payout.
codeabove_daily_limitHTTP422CuándoNo queda tanto límite diario hoy (remaining_today).
codebelow_minimumHTTP422CuándoEl monto es menor que el mínimo por retiro (min_payout, 1 USD por defecto).
codeinvalid_recipientHTTP422CuándoDestino no permitido (ver Seguridad).
codeamount_too_smallHTTP422CuándoCon gas_paid_by: customer, la comisión y el gas se comen el monto.
codenot_hig_walletHTTP422CuándoCon solo billeteras Hig, to no es la billetera de ningún usuario de la app Hig.
codeidempotency_conflictHTTP409CuándoYa hay un retiro con ese external_id y datos distintos.
codehig_check_unavailableHTTP503Cuá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.failedMotivo
error.code en payout.failedinsufficient_balanceMotivoLa caja ya no tenía saldo suficiente al pagar.
error.code en payout.failedpayouts_disabledMotivoLos límites del token se pusieron en 0 después de aceptar el retiro.
error.code en payout.failedabove_max_per_payout / above_daily_limitMotivoLos límites bajaron después de aceptar el retiro.
error.code en payout.failedinvalid_recipientMotivoEl contrato rechazó el destino.
error.code en payout.failedfee_too_high / gas_above_capMotivoLa comisión o el gas superan lo que permite el contrato.
error.code en payout.failedunknown_merchant / relayer_not_authorizedMotivoProblema de configuración de la plataforma.
error.code en payout.failedsend_failedMotivoLa 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}