Documentación del API

Todo lo que necesita tu sistema para recibir depósitos y pagar retiros en USDT (BNB Smart Chain, BEP20) con Payarela. Versión 1.

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.

Los montos viajan siempre como texto decimal ("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:

CabeceraValor
X-Api-Keypk_…
X-Timestampsegundos Unix del momento de la petición (se tolera ±300 s)
X-Noncealeatorio de 16 a 64 caracteres [A-Za-z0-9_-], único por petición
X-Signaturehex( 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 rutaQué haceCuerpo / parámetros
GET /v1/healthestado del servicio (sin firma)—
POST /v1/addressesdirecció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/depositsdepósitos acreditados?user_ref=&asset=&since=ISO&limit=100
GET /v1/deposits/{id}un depósito—
POST /v1/payoutspedir un retiro{"idempotency_key": "retiro-555", "address": "0x…", "amount": "38.15", "asset": "USDT", "user_ref": "usuario-123"}
POST /v1/payouts/batchhasta 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/payoutslistar retiros?since=&limit=
GET /v1/balancesaldo del proyecto—
GET /v1/feescomisiones vigentes—
GET /v1/eventseventos (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_reasonSignificado
insufficient_balanceel disponible del proyecto no cubre monto + comisión
destination_is_deposit_addressel destino es una dirección de depósito de Payarela
destination_not_allowedcontrato del token, bóveda, tesorería u otra dirección del sistema
amount_below_minimum / amount_above_maximumfuera de los límites por retiro
payouts_disabled / project_not_activeel 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:

CabeceraValor
X-Webhook-Idid del evento (único; ignora repeticiones)
X-Webhook-Timestampsegundos Unix
X-Webhook-Signaturehex( HMAC_SHA256( webhook_secret, id + "." + ts + "." + body ) )
X-Webhook-Attemptnú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": "…" } ] } }
HTTPcode
401unauthenticated, key_revoked, bad_signature, stale_timestamp, replayed_nonce, bad_nonce, bad_timestamp
403ip_not_allowed, project_disabled, wrong_network
404not_found
409idempotency_conflict
422validation, bad_amount
429rate_limited
500internal

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_key el identificador del retiro en tu sistema (por ejemplo retiro-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 id recibido.
  • Una conexión por sitio o sistema: si una se compromete, revocas solo esa.