Panel de socio → Desarrolladores → «Obtener clave API». La clave tiene la forma smk_abcdefghij_plus48chars y se muestra una sola vez. Si la pierdes, genera una nueva: la anterior deja de funcionar de inmediato.
Agrega un encabezado
Authorization: Bearer your_key.
Comprueba el acceso
Consulta el saldo. Si recibes una respuesta JSON con data, la clave fue aceptada.
{
"data": [
{
"asset_id": "usdt", // siempre usdt: no hay otro saldo
"available_atomic": "15000000", // 15 USDT en unidades mínimas (6 decimales)
"available": "15", // el mismo valor en forma decimal
"held_atomic": "0", // reservado para retiros sin terminar
"held": "0"
}
]
}
Si la solicitud falló
Respuesta
Causa
401 unauthorized
Falta la clave, contiene un espacio o está truncada. Después de Bearer debe ir el identificador smk_… completo.
401 key_inactive
La clave fue revocada: se generó una nueva, pero la solicitud sigue usando la anterior.
Cinco reglas
La mayoría de los errores de integración se deben a no cumplir una de estas reglas.
1. Envía los importes como cadenas
Pasa amount (una cadena decimal: "0.001", "15") o amount_atomic (un entero en unidades mínimas: "100000" para 0.001 BTC). Usa solo uno de los dos campos. Las respuestas siempre incluyen ambas formas: amount aparece junto a amount_atomic.
ejemplo
"amount": "0.001" // válido
"amount_atomic": "100000" // también válido
// los dos a la vez: 422
2. Toma asset_id solo de /assets
Códigos permitidos: usdt, btc, xmr. Los valores arbitrarios y los ids de exploradores de blockchain se rechazan con 422.
3. Idempotency-Key en cada solicitud que crea algo
Un id único del intento. Si se corta la conexión y no sabes el resultado, repite la solicitud con la misma clave: se devuelve el resultado original y no se crea una segunda operación. Obligatoria para POST /invoices, POST /payments, POST /payouts/preview y POST /payouts.
ejemplo
Idempotency-Key: order-42-payment // primer intento → 201, creado
Idempotency-Key: order-42-payment // reintento → 200, el mismo recurso
Idempotency-Key: order-42-payment // otro importe → 409 conflict
4. Los campos vacíos se omiten
Los valores null y las cadenas vacías no se incluyen. Un pago que aún no se recibió no tiene actual_amount, credited_amount ni fee. Comprueba si la clave existe, no su valor.
ejemplo
// pago aún no recibido:
{ "payment_id": "pay_…", "status": "awaiting_deposit" }
// credited_amount simplemente no aparece
5. No cierres el pedido con expired
expired no significa que la transferencia nunca vaya a llegar. La dirección se sigue vigilando unas 6 horas más, y una transferencia tardía se acredita. Estados finales con acreditación: confirmed, confirmed_underpaid, confirmed_overpaid.
Escenarios
Cada escenario cubre el ciclo completo: orden de las solicitudes, forma de la respuesta y significado de los campos. Los comentarios en los ejemplos JSON son explicativos y no aparecen en las respuestas reales.
Recibir pagos
El cliente paga en BTC un pedido de 60 USD
Pedido order-42. Necesitas unos 60 USDT; con un precio de BTC de unos 60,000, el cliente debe enviar 0.001 BTC.
1
Obtén la lista de activos disponibles
La respuesta devuelve el código del activo y decimals, la cantidad de dígitos después del punto decimal.
{
"data": [
{
"asset_id": "btc", // ← úsalo en las siguientes solicitudes
"symbol": "BTC", // para mostrar al usuario
"network": "bitcoin", // se usa en los retiros
"decimals": 8, // 1 BTC = 100000000 unidades atómicas
"price_usd": "60000" // orientativo, no sirve para cotizar
},
{
"asset_id": "usdt",
"symbol": "USDT",
"network": "TRON",
"decimals": 6,
"price_usd": "1.00"
}
]
}
2
Fija la tasa por 10 minutos
Fija la tasa mientras el cliente está en la página de pago. En este paso no se crea ningún pago ni dirección: solo se calcula una cotización. La respuesta indica cuántos USDT se acreditarán por 0.001 BTC.
Importe en BTC que se espera del cliente. Se envía como cadena, no como número.
Idempotency-Key
Opcional: no se crea ninguna operación. Puedes enviarla para reutilizar la misma cotización.
respuesta
{
"data": {
"credit_lock_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
// ← guárdalo para el siguiente paso
"asset_id": "btc",
"expected_amount": "0.001", // importe esperado del cliente
"expected_amount_atomic": "100000",
"credit_usdt": "59.4", // ← importe que se acreditará
"credit_usdt_atomic": "59400000",
"expires_at": "2026-08-11T17:20:00+00:00"
// ← después de esta hora credit-preview deja de ser válido
}
}
Muestra al cliente expected_amount: cuántos BTC debe enviar.
3
Crea el pago y obtén la dirección
Este paso crea el pago y devuelve la dirección de depósito. No envíes el importe en esta solicitud: ya quedó fijado en credit-preview en el paso 2.
Id de la respuesta de credit-preview. Si la fijación venció, la API devuelve 422: solicita un nuevo preview.
external_id: "order-42"
Tu número de pedido. Sirve para encontrar el pago más adelante y para evitar dos pagos en un mismo pedido.
Idempotency-Key
Obligatoria. Usa un valor único y estable, por ejemplo el número de pedido con un sufijo.
respuesta
{
"data": {
"payment_id": "pay_01HZX...", // ← id para consultar el estado
"external_id": "order-42", // número de pedido de la solicitud
"asset_id": "btc",
"expected_amount": "0.001",
"credit_usdt": "59.4",
"status": "awaiting_deposit", // esperando la transferencia
"channel": "api",
"deposit_address": "bc1qexampledepositaddress000000000",
// ← dirección que se muestra al cliente
"expires_at": "2026-08-11T18:00:00+00:00",
"created_at": "2026-08-11T17:00:00+00:00"
}
}
Si la red requiere memo, la respuesta incluye deposit_memo. Una transferencia sin memo no se acreditará: muestra ambos campos.
4
Espera el pago
Consulta el pago cada 10–30 segundos. El estado avanza así: awaiting_deposit → detected → processing → confirmed.
Indica al cliente que la red es TRC20. Los USDT enviados por otra red no se acreditarán en esta dirección.
El cliente envió menos de lo solicitado
Se esperaba 0.001 BTC y llegó 0.0009, por ejemplo porque la billetera del cliente descontó la comisión de red del importe.
1
Revisa el pago
Si llegó menos que expected_amount pero el importe alcanza para acreditarse, el estado es confirmed_underpaid y credited_amount es menor que la cotización. Si el importe es demasiado pequeño, no se acredita nada.
Compara credited_amount, no credit_usdt. El primero es lo que se acreditó; el segundo, la cotización para un pago exacto.
respuesta
{
"data": {
"payment_id": "pay_01HZX...",
"external_id": "order-42",
"status": "confirmed_underpaid", // ← pago incompleto, pero los fondos se acreditaron
"asset_id": "btc",
"expected_amount": "0.001", // importe esperado
"actual_amount": "0.0009", // importe que llegó realmente
"credit_usdt": "59.4", // cotización para un pago exacto
"credited_amount": "53.4", // ← importe acreditado realmente
"fee": "0.6",
"created_at": "2026-08-11T17:00:00+00:00"
}
}
Los siguientes pasos dependen de ti: pedir que complete el pago, entregar el pedido en parte o cancelarlo. La API devuelve el importe realmente acreditado.
Nota
No decidas si está «pagado o no» comparando actual_amount con expected_amount. Compara credited_amount con los USDT que necesitas.
El cliente no pagó a tiempo
expires_at ya pasó y no llegó ninguna transferencia. El pago pasa a expired; la dirección se sigue vigilando.
1
Estado expired
El plazo de pago terminó. La dirección se sigue vigilando unas 6 horas.
{
"data": {
"payment_id": "pay_01HZX...",
"status": "expired", // plazo vencido
"asset_id": "btc",
"expected_amount": "0.001",
"deposit_address": "bc1qexampledepositaddress000000000",
"expires_at": "2026-08-11T18:00:00+00:00"
// no hay actual_amount ni credited_amount: no llegó ninguna transferencia
}
}
2
El cliente pagó tarde
Si la transferencia llega durante el plazo adicional de vigilancia, el pago igualmente pasa a confirmed. No elimines el pedido con expired: márcalo como atrasado y sigue consultando.
No emitas una dirección nueva justo después de expired: puede que el cliente ya esté enviando a la anterior. Seguimos esperando la transferencia 6 horas más.
Facturas
Una factura en USD con página de pago alojada. El cliente elige la red y tú recibes USDT.
Una factura de 15 USD pagada en USDT
Pedido order-15. El comercio crea una factura en USD y envía al cliente un enlace a la página de pago.
1
Crea la factura
La factura fija el importe en USD y quién paga la comisión, y devuelve pay_url. Todavía no hay dirección.
Importe del pedido como cadena. Este endpoint solo acepta USD, no amount_atomic.
fee_on
merchant: tú pagas la comisión y el cliente ve 15.00. customer: el cliente paga la comisión.
Idempotency-Key
Obligatoria. Un reintento con la misma clave devuelve la misma factura.
partner_ref
Opcional. El Partner ID del integrador que configuró tu integración (programa de referidos de SnapEX Pay). Vincula tu cuenta a ese socio una sola vez; omítelo si haces la integración tú mismo.
Un pago incompleto no cierra la factura: el estado es underpaid y la misma dirección espera el resto (USDT: 72 horas). Si el resto no llega a tiempo, la factura termina como partially_paid.
Nota
No menciones rutas internas en la interfaz para el cliente. La página de pago muestra solo la dirección, el importe y la red.
No hay API de reembolsos. Una segunda transferencia completada es una acreditación aparte, no una reversión de la primera.
Retiros
Transferencias salientes desde tu saldo en USDT.
Envía al cliente 15 USDT en BTC
amount es lo que se descuenta de tu saldo, no lo que recibe el destinatario. El importe del destinatario lo devuelve preview.
1
Comprueba el saldo disponible
Usa available, no available más held. held está reservado para retiros sin terminar y no se puede gastar.
USDT que se descuentan de tu saldo. No es el importe que recibe el destinatario.
destination_asset_id
Activo del destinatario. Código de GET /assets.
destination_network
Red del destinatario. Para BTC, bitcoin. Tómala de network en GET /assets.
destination_address
Dirección del destinatario. Verifícala antes de enviar: la transferencia no se puede cancelar.
Idempotency-Key
Obligatoria. Usa una clave aparte para el preview.
respuesta
{
"data": {
"preview_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
// ← id para el siguiente paso
"from_asset_id": "usdt",
"from_amount": "15", // importe descontado
"to_asset_id": "btc",
"to_amount": "0.000221", // ← importe que recibe el destinatario
"fee_amount": "0.000019", // comisión en BTC, ya incluida
"expires_at": "2026-08-11T17:20:00+00:00"
// ← ejecútalo antes de esta hora
}
}
Muestra to_amount al usuario como el importe que recibirá. Si el importe no es aceptable, no ejecutes el preview: vence en expires_at.
3
Ejecuta el retiro
Este paso descuenta y envía. Repite la dirección y la red en la solicitud: así se evita una sustitución entre pasos.
Rechazo por regla de negocio. El envoltorio es el mismo que el de los errores de validación.
details.amount
El error está asociado a un campo y se puede mostrar en el formulario.
respuesta
{
"error": {
"code": "validation_error",
"message": "The request is invalid.",
"details": {
"amount": ["Saldo insuficiente."]
// ← el motivo del rechazo está asociado a amount
}
}
}
No se reserva nada. Reintentar con la misma Idempotency-Key es seguro; primero recarga el saldo.
Nota
Ten en cuenta held: si parte de los fondos está reservada para otro retiro, available será menor de lo que esperas.
Operaciones
Historial de pagos
El ledger es un registro de operaciones: las acreditaciones, los descuentos y las reservas se guardan como entradas separadas.
1
Obtén el historial por páginas
Las entradas se devuelven de la más reciente a la más antigua. Hasta 100 registros por solicitud; la página siguiente usa el cursor.
La operación ya existía; se devuelve el objeto original.
respuesta
{
"data": {
"payout_id": "payout_01HZX...", // ← el mismo id que en la primera respuesta
"external_id": "wd-77",
"debit_amount": "15",
"status": "processing", // el estado puede haber cambiado durante la espera
"created_at": "2026-08-11T17:10:00+00:00"
}
}
HTTP 201: la operación se creó en esta solicitud. HTTP 200: ya existía. Ambos códigos indican éxito.
2
Si cambiaron los parámetros
Si repites la misma clave con otro importe u otra dirección, la solicitud se rechaza. Así se evitan parámetros que no coinciden.
Conflicto: la clave ya está asociada a otra operación.
Qué hacer
Si es un retiro nuevo, usa una Idempotency-Key nueva.
respuesta
{
"error": {
"code": "conflict",
"message": "Idempotency-Key was used with different parameters."
}
}
Nota
Genera la clave una sola vez y guárdala junto con el pedido. Si generas una clave nueva antes de cada envío, pierdes la protección contra duplicados.
Ante un HTTP 429, reintenta con la misma clave tras unos segundos.
Webhooks
Cuando cambia una factura, un depósito, un retiro, una conversión o el saldo, SnapEX envía una solicitud POST a tu URL. Un webhook es una señal, no la fuente de verdad: verifica la firma y, para decisiones importantes, vuelve a consultar el objeto mediante la API.
Configuración
Configura la URL en el panel
Panel de socio → Desarrolladores → tarjeta «Webhook»: ingresa la URL y pulsa «Guardar». Una URL por cuenta; recibe todos los tipos de eventos de la tabla de abajo.
Guarda el secreto de firma
La primera vez que guardas, el panel muestra el secreto whsec_… una sola vez. Guárdalo solo en tu servidor. Si después cambias la URL, el secreto se mantiene.
Requisitos de la URL
Una URL absoluta https:// con un host público: sin login:password en la URL, sin localhost, y el host no debe resolver a una IP privada o reservada. La URL se verifica al guardar y otra vez antes de cada intento de entrega.
Qué llega
Encabezado
Significado
Webhook-Id
Id único del evento (UUID), igual al id del cuerpo. No cambia en los reintentos ni en los reenvíos manuales: deduplica por este valor.
Webhook-Timestamp
Hora Unix en segundos en que se envió este intento. Cada reintento lleva un valor nuevo y una firma nueva.
Webhook-Signature
v1= seguido del HMAC-SHA256 en hex en minúsculas de la cadena «{Webhook-Timestamp}.{raw body}». La clave es el secreto completo, incluido el prefijo whsec_.
Content-Type
Siempre application/json. El cuerpo es JSON en UTF-8.
{
"id": "6f1c2a8e-3b4d-4f5a-9c7e-1a2b3c4d5e6f", // = Webhook-Id
"type": "invoice.paid", // tipo de evento, ver la tabla de abajo
"created_at": "2026-09-25T12:00:10+00:00", // cuándo ocurrió el evento
"data": { // el objeto en el momento del evento
"invoice_id": "INV-8K2M1Q0P",
"environment": "live",
"amount_usd": "15.00",
"charge_usd": "15.00",
"credit_usd": "13.85",
"status": "paid",
"external_id": "order-15",
"paid_at": "2026-09-25T12:00:10+00:00"
}
}
data depende del tipo: invoice.*: la factura con la misma forma que GET /invoices/{id} (attempts está vacío, el intento actual está en payment); payment.*: el depósito (payment_id, external_id, status, importes); payout.*: el retiro (payout_id, external_id, status, importes); conversion.*: la conversión (conversion_id, status, importes); balance.changed: asset_id, entry_type, available_atomic y held_atomic después del cambio.
Tipos de eventos
Evento
Significado
invoice.created
Se creó la factura; todavía no se eligió un método de pago (open).
invoice.awaiting
Se eligió un método de pago y se emitió la dirección (awaiting).
invoice.underpaid
Llegó parte del importe; la misma dirección espera el resto (underpaid).
invoice.paid
Se pagó y acreditó el importe completo (paid, final). La única señal para entregar el pedido.
invoice.partially_paid
El cliente pagó solo una parte, terminó el plazo para completar el pago y la parte recibida se te acreditó (partially_paid, final).
invoice.expired
El plazo de la factura terminó sin pago (expired, final).
invoice.extra_payment
Llegó otra transferencia después de que se pagó la factura; se acredita por separado.
payment.created
Se creó un depósito.
payment.detected
La transferencia se vio en la red, pero todavía no se acreditó.
payment.credited
Se acreditó el depósito (uno de los estados confirmed*).
payment.expired
Venció el plazo del depósito.
payout.created
Se creó un retiro y se reservaron los fondos.
payout.processing
El retiro se está enviando.
payout.completed
Los fondos se entregaron al destinatario.
payout.failed
El retiro falló.
conversion.completed
La conversión se completó.
conversion.failed
La conversión falló total o parcialmente.
balance.changed
Se registró un movimiento de saldo: acreditación, reserva o descuento.
Entrega el pedido solo con invoice.paid: se envía únicamente cuando se pagó el importe completo. invoice.underpaid e invoice.partially_paid significan que el importe está incompleto.
Las facturas creadas con environment=test son simuladas: pasan por los mismos estados y envían los mismos webhooks (data.environment = "test"), pero nunca modifican el saldo real.
Entrega y reintentos
Se considera éxito cualquier respuesta 2xx. Cualquier otro código, un tiempo de espera agotado o un error de red es un fallo. El cuerpo de la respuesta no se interpreta.
Tiempos de espera: 5 s para conectar, 15 s para toda la solicitud.
Las redirecciones no se siguen: un 301/302 cuenta como fallo. Configura la URL final.
Hasta 5 intentos por evento: el primero y luego reintentos unos 10 s, 1 min, 5 min y 15 min después de cada fallo. Tras el quinto fallo, la entrega automática se detiene.
En la misma página, la tarjeta «Entregas del webhook» muestra: evento, código HTTP o error, hora y número de intento. El botón «Reenviar» vuelve a enviar el evento con el mismo Webhook-Id.
La entrega es at-least-once (al menos una vez): un evento puede llegar varias veces y los eventos pueden llegar desordenados (un evento reintentado puede llegar después de uno más reciente).
Verificación de la firma
Lee el cuerpo sin procesar de la solicitud como bytes, antes de parsear el JSON. Un JSON vuelto a serializar no coincidirá con la firma.
Calcula el HMAC-SHA256 de «{Webhook-Timestamp}.{raw body}» con tu secreto whsec_… como clave y codifícalo en hex.
Compara «v1=» + hex con el encabezado Webhook-Signature usando una función de tiempo constante: hash_equals, crypto.timingSafeEqual, hmac.compare_digest.
Rechaza un Webhook-Timestamp que difiera de tu reloj en más de 5 minutos (recomendado). Los reintentos llevan un timestamp nuevo, así que esto no los afecta.
PHP
<?php
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // whsec_… del panel
$body = file_get_contents('php://input'); // cuerpo sin procesar, antes de parsear el JSON
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (! ctype_digit($timestamp)
|| abs(time() - (int) $timestamp) > 300
|| ! hash_equals($expected, $signature)) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// omitir si este Webhook-Id ya se procesó; hacer el trabajo pesado en una cola
http_response_code(200);
Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
const secret = process.env.SNAPEX_WEBHOOK_SECRET; // whsec_… del panel
// cuerpo sin procesar, antes de parsear el JSON
app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('Webhook-Timestamp') || '';
const signature = Buffer.from(req.get('Webhook-Signature') || '');
const expected = Buffer.from('v1=' + crypto.createHmac('sha256', secret)
.update(timestamp + '.').update(req.body).digest('hex'));
const ok = /^\d+$/.test(timestamp)
&& Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300
&& signature.length === expected.length
&& crypto.timingSafeEqual(signature, expected);
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// omitir si este Webhook-Id ya se procesó; hacer el trabajo pesado en una cola
res.sendStatus(200);
});
Python
import hashlib, hmac, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["SNAPEX_WEBHOOK_SECRET"].encode() # whsec_… del panel
@app.post("/hooks/snapex")
def snapex_webhook():
body = request.get_data() # cuerpo sin procesar, antes de parsear el JSON
timestamp = request.headers.get("Webhook-Timestamp", "")
signature = request.headers.get("Webhook-Signature", "")
expected = "v1=" + hmac.new(SECRET, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
if not (timestamp.isascii() and timestamp.isdigit()) \
or abs(time.time() - int(timestamp)) > 300 \
or not hmac.compare_digest(expected.encode(), signature.encode()):
abort(400)
event = request.get_json()
# omitir si este Webhook-Id ya se procesó; hacer el trabajo pesado en una cola
return "", 200
Lista de comprobación del handler
Verifica la firma sobre el cuerpo sin procesar con una comparación de tiempo constante; responde 400 si no coincide.
Rechaza las solicitudes con un Webhook-Timestamp de más de 5 minutos de antigüedad.
Guarda los Webhook-Id procesados e ignora los repetidos. No dependas del orden: compara el estado del evento con el que ya tienes.
Responde 2xx rápido, muy por debajo de los 15 segundos, y haz el trabajo pesado de forma asíncrona.
Después de un webhook puedes volver a consultar el objeto mediante la API para confirmarlo: GET /invoices/{id}, GET /payments/{id}, GET /payouts/{id}.
Estados
La etiqueta «final» indica que el estado ya no cambiará. La única excepción son los depósitos vencidos: todavía puede llegar una transferencia tardía.
Depósitos
Estado
Significado
Qué hacer
awaiting_deposit
Dirección emitida, esperando la transferencia
Muestra al cliente la dirección y el importe
detected
Transferencia vista en la red, todavía sin acreditar
Espera. No cierres el pedido
processing
Conversión a USDT en curso
Espera
confirmed
Llegó el importe esperado, fondos acreditados
Cierra el pedido
final
confirmed_underpaid
Llegó menos de lo esperado, dentro del margen permitido
Revisa credited_amount y decide si es suficiente
final
confirmed_overpaid
Llegó más de lo esperado, acreditado según credit-preview
Cierra el pedido. Contacta con soporte por el excedente
final
confirmed_mismatch
Se mantiene por compatibilidad
No se asigna a los pagos nuevos
final
manual_review
Requiere revisión manual
Espera la revisión; no crees un segundo pago
expired
Venció el plazo de pago; la dirección se vigila ~6 h más
No elimines el pedido; sigue consultando
failed
Fallo del lado del proveedor
Crea un pago nuevo
final
cancelled
Pago cancelado
Crea un pago nuevo
final
refunded
Reembolsado por el proveedor
Concilia el saldo; contacta con soporte si no coincide
final
Facturas
Estado
Significado
Qué hacer
open
Factura creada, método de pago sin elegir
Da al cliente pay_url o elige tú el asset_id
awaiting
Dirección emitida, esperando la transferencia
Muestra la dirección y el importe; consulta el estado
underpaid
Llegó parte del importe; misma dirección
Pide al cliente que envíe el resto a esta dirección
paid
Se acreditó la primera transferencia completa
Cierra el pedido
final
partially_paid
El cliente pagó solo una parte, terminó el plazo para completar el pago y la parte recibida se te acreditó
No lo entregues como pedido pagado; acuerda el resto con el cliente
final
expired
Terminó el plazo de la factura
Crea una factura nueva
final
Retiros
Estado
Significado
Qué hacer
reserved
Fondos reservados para el retiro
Espera
submitting
Enviado al proveedor
Espera
processing
El proveedor está enviando on-chain
Espera
completed
Fondos entregados al destinatario
Listo
final
manual_review
El resultado no es claro
No crees otro retiro; espera la revisión
failed
La transferencia falló; por lo general, la reserva se libera
Revisa el saldo; si hace falta, reintenta con una Idempotency-Key nueva
final
Errores
Una respuesta correcta siempre viene en data. Un error siempre viene en error. No hay otros envoltorios.
forma del error
{
"error": {
"code": "validation_error", // código legible por máquina, ver la tabla de abajo
"message": "The request is invalid.",
"details": { // no siempre está presente
"destination_address": ["The destination address field is required."]
// la clave es el campo que no pasó la validación
}
}
}
HTTP
code
Cuándo
Qué hacer
401
unauthorized
Falta la clave o no es válida
Revisa el encabezado Authorization: Bearer smk_… completo, sin espacios ni saltos de línea
401
key_inactive
Clave revocada
La clave anterior fue revocada. Usa la clave vigente del panel
401
key_expired
Clave vencida
Genera una clave nueva
404
not_found
El id no existe o pertenece a otro comercio
Usa el payment_id o payout_id de tu propia respuesta
409
conflict
La Idempotency-Key está asociada a otra operación, o el external_id ya está en uso
Para una operación nueva, usa una clave nueva. Para un reintento, usa la misma clave y los mismos parámetros
422
validation_error
Campos no válidos, credit-preview vencido, saldo insuficiente o falta la Idempotency-Key
Consulta details para ver el campo y el motivo
429
rate_limited
Se superó el límite de solicitudes
Reintenta más tarde con la misma Idempotency-Key
500
internal_error
Error interno del servidor
Reintenta más tarde con la misma Idempotency-Key. Si persiste, contacta con soporte e incluye el X-Request-Id