Cómo funciona
Cada usuario de tu proyecto recibe una dirección fija de depósito (no vence, no cambia). Todo USDT que llegue ahí se acredita al saldo de tu proyecto tras 15 confirmaciones, descontando tu comisión de depósito, y te avisamos por webhook. Para pagar a un usuario pides un retiro con una clave única: si la petición se repite por un corte de red, no se paga dos veces. Tu saldo queda en custodia en Payarela y lo retiras a tu wallet cuando quieras desde el portal.
"38.15"), nunca como número flotante, para no perder centavos. Todas las respuestas son JSON.Cuenta y conexiones
Tu cuenta en Payarela tiene un saldo y tantas conexiones como sitios o sistemas quieras conectar (como en otros procesadores: una cuenta, varias integraciones). Cada conexión es una clave de API con su nombre, y puede tener su propio webhook y sus propias IPs permitidas; si no los define, usa los del proyecto. Los depósitos y retiros quedan asociados a la conexión que los creó, y los avisos van al webhook de esa conexión. Las creas y revocas desde el portal, en Conexiones.
Claves y firma
Cada conexión tiene un identificador público (pk_…) y un secreto (sk_…) que se muestra una sola vez. Cada petición lleva estas cabeceras:
| Cabecera | Valor |
|---|---|
X-Api-Key | pk_… |
X-Timestamp | segundos Unix del momento de la petición (se tolera ±300 s) |
X-Nonce | aleatorio de 16 a 64 caracteres [A-Za-z0-9_-], único por petición |
X-Signature | hex( HMAC_SHA256( secreto, ts + "\n" + nonce + "\n" + MÉTODO + "\n" + ruta_con_query + "\n" + sha256hex(cuerpo) ) ) |
El cuerpo se firma byte a byte (en GET el cuerpo es vacío: sha256("")). Un nonce repetido dentro de la ventana se rechaza (replayed_nonce). Si la conexión o el proyecto tienen IPs permitidas, cualquier otra IP recibe ip_not_allowed. Límites: 600 peticiones por minuto por IP y 300 por minuto por clave (rate_limited).
# ejemplo con curl (GET /v1/balance)
TS=$(date +%s); NONCE=$(openssl rand -hex 12)
SIG=$(printf '%s\n%s\nGET\n/v1/balance\n%s' "$TS" "$NONCE" "$(printf '' | sha256sum | cut -d' ' -f1)" | openssl dgst -sha256 -hmac "sk_…" | sed 's/^.* //')
curl https://api.payarela.com/v1/balance -H "X-Api-Key: pk_…" -H "X-Timestamp: $TS" -H "X-Nonce: $NONCE" -H "X-Signature: $SIG"
Rutas del API
Base: https://api.payarela.com
| Método y ruta | Qué hace | Cuerpo / parámetros |
|---|---|---|
GET /v1/health | estado del servicio (sin firma) | — |
POST /v1/addresses | dirección fija de depósito del usuario (la crea si no existe) | {"user_ref": "usuario-123"} |
GET /v1/addresses/{user_ref} | consultar la dirección | — |
GET /v1/deposits | depósitos acreditados | ?user_ref=&asset=&since=ISO&limit=100 |
GET /v1/deposits/{id} | un depósito | — |
POST /v1/payouts | pedir un retiro | {"idempotency_key": "retiro-555", "address": "0x…", "amount": "38.15", "asset": "USDT", "user_ref": "usuario-123"} |
POST /v1/payouts/batch | hasta 50 retiros en una llamada | {"payouts": [ … ]} → results[] con ok por cada uno |
GET /v1/payouts/{id} · GET /v1/payouts/by-key/{key} | estado de un retiro | — |
GET /v1/payouts | listar retiros | ?since=&limit= |
GET /v1/balance | saldo del proyecto | — |
GET /v1/fees | comisiones vigentes | — |
GET /v1/events | eventos (los mismos de los webhooks), para recuperar los perdidos | ?since=&limit= |
user_ref admite letras, números y . _ @ : - (máximo 128). La idempotency_key admite letras, números y . _ : - (máximo 128). Los identificadores que empiezan por _ están reservados.
Monedas
Monedas activas en esta instalación: USDT, USDC. Todas en BNB Smart Chain (BEP20) y en la misma dirección de depósito de cada usuario.
Cada depósito, retiro y evento trae el campo asset (USDT o USDC). Los saldos son separados por moneda: un retiro en USDC sale del saldo de USDC. Al pedir un retiro, asset es opcional y vale USDT si no lo mandas; así las integraciones que ya existen siguen igual.
Una moneda que no está activa se rechaza con 400 asset_not_enabled. Usar la misma idempotency_key con otra moneda es 409 idempotency_conflict, nunca un retiro nuevo.
Direcciones de depósito
{ "user_ref": "usuario-123", "address": "0x4f…", "network": "bsc-testnet",
"token": "0x22d945426e282c27e2455344c58388c4091bc55f", "token_symbol": "USDT",
"accepts": [{ "asset": "USDT", "token": "0x22d945426e282c27e2455344c58388c4091bc55f" }, { "asset": "USDC", "token": "0xadda355cf0fd9794a46c527dcda3b6bd987f9f4b" }], "created_at": "…" }
Muestra la dirección al usuario con su código QR y el aviso de red: solo USDT por BNB Smart Chain (BEP20). Otras monedas u otras redes enviadas a esa dirección se pierden. La dirección es la misma para siempre: no hay facturas que venzan ni montos exactos que exigir; lo que llegue se acredita.
Retiros
Respuesta 201 al crear, 200 si la clave ya existía (es el mismo retiro: no se vuelve a pagar). 409 idempotency_conflict si la misma clave llega con otro destino u otro monto.
{ "id": "…", "idempotency_key": "retiro-555", "asset": "USDT", "address": "0xab…", "amount": "38.15", "fee": "0.4815",
"status": "queued", "reject_reason": null, "tx_hash": null, "block_number": null,
"created_at": "…", "sent_at": null, "confirmed_at": null }
Estados: queued → processing → sent → confirmed | failed; o rejected desde el inicio con reject_reason:
| reject_reason | Significado |
|---|---|
insufficient_balance | el disponible del proyecto no cubre monto + comisión |
destination_is_deposit_address | el destino es una dirección de depósito de Payarela |
destination_not_allowed | contrato del token, bóveda, tesorería u otra dirección del sistema |
amount_below_minimum / amount_above_maximum | fuera de los límites por retiro |
payouts_disabled / project_not_active | el proyecto no tiene retiros habilitados |
Un retiro failed devuelve monto y comisión al disponible del proyecto. Para reintentarlo, se pide uno nuevo con otra clave. La comisión de retiro se descuenta del saldo del proyecto, aparte del monto: el destinatario recibe exactamente amount.
Saldo y comisiones
{ "available": "1234.56", "reserved": "38.15", "total_deposited": "9000", "total_deposit_fees": "90",
"payouts_enabled": true, "token_symbol": "USDT", "network": "bsc-testnet",
"balances": [{ "asset": "USDT", "available": "…", "reserved": "…", "total_deposited": "…", "total_deposit_fees": "…" },
{ "asset": "USDC", "available": "…", "reserved": "…", "total_deposited": "…", "total_deposit_fees": "…" }] }
reserved es lo comprometido en retiros en curso. GET /v1/fees devuelve el porcentaje de depósito, el de retiro y el fijo por retiro vigentes para tu proyecto.
Webhooks
Configura una URL https de tu sistema en la conexión (o como valor por defecto del proyecto). Payarela hace POST con JSON y estas cabeceras:
| Cabecera | Valor |
|---|---|
X-Webhook-Id | id del evento (único; ignora repeticiones) |
X-Webhook-Timestamp | segundos Unix |
X-Webhook-Signature | hex( HMAC_SHA256( webhook_secret, id + "." + ts + "." + body ) ) |
X-Webhook-Attempt | número de intento |
{ "id": "…", "type": "deposit.confirmed", "created_at": "…",
"data": { "id": "…", "asset": "USDT", "user_ref": "usuario-123", "address": "0x4f…", "tx_hash": "0x…",
"amount": "38.15", "fee": "0.3815", "net": "37.7685", "block_number": 123 } }
Tipos: deposit.confirmed, payout.sent, payout.confirmed, payout.failed (el data de los retiros es el mismo objeto que devuelve GET /v1/payouts/{id}).
Reglas: responde 2xx en menos de 10 segundos (procesa en segundo plano si hace falta). Si no, se reintenta con esperas de 1, 2, 5, 10, 15, 30, 60, 120, 240, 480, 720 y 1440 minutos. Verifica siempre la firma y la ventana de tiempo (±300 s). Guarda el id procesado para no acreditar dos veces. Si se perdió algo, GET /v1/events?since= lo devuelve.
Errores
{ "error": { "code": "validation", "message": "…", "details": [ { "path": "amount", "message": "…" } ] } }
| HTTP | code |
|---|---|
| 401 | unauthenticated, key_revoked, bad_signature, stale_timestamp, replayed_nonce, bad_nonce, bad_timestamp |
| 403 | ip_not_allowed, project_disabled, wrong_network |
| 404 | not_found |
| 409 | idempotency_conflict |
| 422 | validation, bad_amount |
| 429 | rate_limited |
| 500 | internal |
Cliente PHP
Sin dependencias (cURL y hash_hmac, PHP 8.0 o superior). Descargar PayarelaClient.php.
require 'PayarelaClient.php';
use Payarela\PayarelaClient;
$payarela = new PayarelaClient('https://api.payarela.com', 'pk_…', 'sk_…', 'whsec_…');
// dirección fija del usuario (muéstrala con su QR)
$dir = $payarela->depositAddress('usuario-123'); // ['address' => '0x…', 'network' => '…', …]
// retiro idempotente: la clave evita el doble pago
$retiro = $payarela->createPayout('retiro-555', '0xabc…', '38.15', 'usuario-123');
$estado = $payarela->payout($retiro['id']); // queued | processing | sent | confirmed | failed
// saldo del proyecto
$saldo = $payarela->balance();
// webhook (en tu endpoint https): verifica la firma y procesa una sola vez por id
$cuerpo = file_get_contents('php://input');
$evento = $payarela->verifyWebhook($cuerpo, getallheaders()); // lanza excepción si la firma no es válida
if ($evento['type'] === 'deposit.confirmed') { /* acreditar $evento['data']['net'] al usuario … */ }
http_response_code(200);
Modo de pruebas
El mismo API existe en la red de pruebas de BSC con claves pk_test_…. Ahí el token es un USDT de prueba sin valor; el flujo (dirección, depósito, webhook, retiro) es idéntico al real. Este servidor está en la red de pruebas.
Buenas prácticas
- Guarda el secreto de API y el de webhook fuera del código (variables de entorno). Si se filtran, revoca la conexión desde el portal y crea otra: el resto sigue funcionando.
- Usa como
idempotency_keyel identificador del retiro en tu sistema (por ejemploretiro-555): así un reintento nunca paga dos veces. - Acredita depósitos solo desde los webhooks o desde
GET /v1/deposits, nunca desde lo que diga el usuario. - Limita cada conexión a la IP de su servidor (Conexiones → IPs permitidas).
- Responde rápido a los webhooks y procesa después; registra cada
idrecibido. - Una conexión por sitio o sistema: si una se compromete, revocas solo esa.

