ES

Documentación de la API de comercio

URL base: https://snapex.pro/api/v1/merchant

Clave API y primera solicitud

  1. Obtén una clave en el panel

    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.

  2. Agrega un encabezado

    Authorization: Bearer your_key.

  3. Comprueba el acceso

    Consulta el saldo. Si recibes una respuesta JSON con data, la clave fue aceptada.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/balances \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"
respuesta
{
  "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.

ejemplo
"asset_id": "btc"   // correcto
"asset_id": "BTC"   // error
"asset_id": "bitcoin" // error

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.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/assets \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

GET /assets
No hace falta ningún parámetro.
Authorization
El único encabezado obligatorio de la API.
respuesta
{
  "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.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments/credit-preview \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"asset_id":"btc","amount":"0.001"}'

Puntos clave

asset_id: "btc"
Código del paso anterior.
amount: "0.001"
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.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: order-42-payment" \
  -d '{
    "credit_lock_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "external_id": "order-42"
  }'

Puntos clave

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

solicitud
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

GET /payments/{payment_id}
Usa el payment_id de la respuesta anterior.
Cuándo
El intervalo recomendado es de 10–30 segundos. El límite es de 120 solicitudes por minuto por clave; no consultes más rápido.
respuesta
{
  "data": {
    "payment_id": "pay_01HZX...",
    "status": "detected",              // transferencia vista en la red,
                                       // todavía sin confirmaciones suficientes
    "asset_id": "btc",
    "expected_amount": "0.001",
    "actual_amount": "0.001",          // ← importe que llegó realmente
    "deposit_address": "bc1qexampledepositaddress000000000",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}
5
Pago confirmado

confirmed significa que los fondos se acreditaron al saldo. Puedes cerrar el pedido.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

status: confirmed
Estado final; no cambiará.
credited_amount
USDT acreditados realmente. En un pago exacto coincide con credit_usdt.
respuesta
{
  "data": {
    "payment_id": "pay_01HZX...",
    "external_id": "order-42",
    "status": "confirmed",             // ← acreditación completada
    "asset_id": "btc",
    "expected_amount": "0.001",
    "actual_amount": "0.001",          // llegó el importe esperado
    "credit_usdt": "59.4",
    "credited_amount": "59.4",         // ← importe acreditado al saldo
    "fee": "0.6",                      // comisión, ya incluida en credited_amount
    "deposit_address": "bc1qexampledepositaddress000000000",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}

Concilia por credited_amount, no por expected_amount: si el pago es incompleto, los valores difieren.

Nota

  • No crees el pago por adelantado: la tasa queda fijada solo 10 minutos y la dirección se emite para un importe concreto.
  • No cierres el pedido con detected: los fondos todavía no se acreditaron.

El cliente paga en USDT

El flujo es el mismo que en el escenario con BTC; solo cambia asset_id. Abajo hay un ejemplo más corto.

1
Fija el importe

No hay conversión, así que credit_usdt es casi el importe menos la comisión.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments/credit-preview \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -d '{"asset_id":"usdt","amount":"100"}'

Puntos clave

asset_id: "usdt"
La única diferencia con el escenario de BTC.
amount: "100"
El cliente envía 100 USDT en TRON.
respuesta
{
  "data": {
    "credit_lock_id": "b2c3d4e5-...",
    "asset_id": "usdt",
    "expected_amount": "100",
    "credit_usdt": "99",         // ← comisión de 1 USDT
    "expires_at": "2026-08-11T17:20:00+00:00"
  }
}
2
Crea el pago

El mismo paso que antes. La dirección es de TRON (empieza por T).

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-43-payment" \
  -d '{"credit_lock_id":"b2c3d4e5-...","external_id":"order-43"}'

Puntos clave

Idempotency-Key
Usa una Idempotency-Key nueva para cada pedido nuevo. No reutilices la clave de order-42.
respuesta
{
  "data": {
    "payment_id": "pay_01HZY...",
    "external_id": "order-43",
    "asset_id": "usdt",
    "expected_amount": "100",
    "credit_usdt": "99",
    "status": "awaiting_deposit",
    "deposit_address": "TXYZexampletronaddress000000000",
                                 // ← dirección de TRON, TRC20
    "expires_at": "2026-08-11T18:00:00+00:00"
  }
}

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.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

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

solicitud
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

status: expired
El plazo de pago venció.
credited_amount
Todavía no llegaron fondos.
respuesta
{
  "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.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

expired → confirmed
Caso normal: la transferencia llegó después de expires_at.
respuesta
{
  "data": {
    "payment_id": "pay_01HZX...",
    "status": "confirmed",             // ← acreditado después de expires_at
    "actual_amount": "0.001",
    "credited_amount": "59.4",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}

Nota

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

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/invoices \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: order-15-invoice" \
  -d '{
    "amount_usd": "15.00",
    "fee_on": "merchant",
    "external_id": "order-15"
  }'

Puntos clave

amount_usd
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.
respuesta
{
  "data": {
    "invoice_id": "INV-8K2M1Q0P",
    "amount_usd": "15.00",
    "fee_on": "merchant",
    "charge_usd": "15.00",
    "credit_usd": "13.85",
    "status": "open",
    "pay_url": "https://pay.snapex.pro/0123456789abcdef0123456789abcdef",
    "methods": [
      { "asset_id": "usdt.trc20", "symbol": "USDT", "network": "TRC20" },
      { "asset_id": "usdt.erc20", "symbol": "USDT", "network": "ERC20" },
      { "asset_id": "usdt.bep20", "symbol": "USDT", "network": "BEP20" }
    ]
  }
}

La clave necesita invoices.write o payments.write. Entorno de prueba: environment=test.

2
El cliente elige la red

Este paso emite una dirección única. El método se puede cambiar hasta que llegue una transferencia.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/invoices/INV-8K2M1Q0P/methods \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-15-trc20" \
  -d '{"asset_id":"usdt.trc20"}'

Puntos clave

asset_id: "usdt.trc20"
Un código de methods: usdt.trc20, usdt.erc20 o usdt.bep20. Las demás monedas salen de la misma lista.
payment.address
Muestra solo esta dirección y este importe. No se usan Memo ni Tag.
respuesta
{
  "data": {
    "invoice_id": "INV-8K2M1Q0P",
    "status": "awaiting",
    "selected_asset_id": "usdt.trc20",
    "payment": {
      "asset_id": "usdt.trc20",
      "amount": "15 USDT",
      "address": "TExampleUniqueAddressForThisAttempt",
      "status": "awaiting"
    }
  }
}

Puedes omitir esta llamada: el cliente elige la red en pay_url.

3
La factura está pagada

La primera transferencia completada cierra la factura. Una transferencia posterior a otra dirección se acredita por separado.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/invoices/INV-8K2M1Q0P \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

status: paid
Estado final. Puedes cerrar el pedido.
invoice.paid
El webhook invoice.paid llega a la URL configurada en el panel. Verificación de la firma: consulta la sección «Webhooks».
respuesta
{
  "data": {
    "invoice_id": "INV-8K2M1Q0P",
    "status": "paid",
    "credit_usd": "13.85",
    "paid_at": "2026-09-25T12:00:10+00:00"
  }
}

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.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/balances \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

available
Saldo disponible para descontar.
held
Reservado para retiros en curso.
respuesta
{
  "data": [
    {
      "asset_id": "usdt",
      "available": "74.4",       // ← disponible para gastar
      "available_atomic": "74400000",
      "held": "0",               // nada reservado
      "held_atomic": "0"
    }
  ]
}
2
Cotiza el retiro

El preview devuelve cuántos BTC recibe el destinatario por 15 USDT y la comisión. Todavía no se descuenta nada: solo se fija la cotización.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: wd-77-preview" \
  -d '{
    "destination_asset_id": "btc",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "amount": "15"
  }'

Puntos clave

amount: "15"
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.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: wd-77-execute" \
  -d '{
    "preview_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "external_id": "wd-77"
  }'

Puntos clave

Idempotency-Key: wd-77-execute
Una clave nueva, distinta de la del preview. Si reutilizas la clave del preview, recibes 409.
destination_network / address
Debe coincidir exactamente con el preview; de lo contrario, 422.
external_id
Tu id de la operación para buscarla después.
respuesta
{
  "data": {
    "payout_id": "payout_01HZX...",  // ← id para el seguimiento
    "external_id": "wd-77",
    "asset_id": "usdt",
    "debit_amount": "15",            // descontado del saldo
    "destination_asset_id": "btc",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "destination_amount": "0.000221",// importe que recibe el destinatario
    "fee": "0.000019",
    "status": "reserved",            // fondos reservados, envío iniciado
    "channel": "api",
    "created_at": "2026-08-11T17:10:00+00:00"
  }
}
4
Espera a que se complete

Secuencia de estados: reserved → submitting → processing → completed. Consulta cada 15–30 segundos.

solicitud
curl -sS https://snapex.pro/api/v1/merchant/payouts/payout_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

status: completed
Fondos entregados. Estado final.
status: failed
La transferencia falló; por lo general, la reserva vuelve al saldo.
status: manual_review
El resultado no es claro. Espera la revisión; no crees otro retiro.
respuesta
{
  "data": {
    "payout_id": "payout_01HZX...",
    "external_id": "wd-77",
    "debit_amount": "15",
    "destination_amount": "0.000221",
    "status": "completed",           // ← entregado
    "created_at": "2026-08-11T17:10:00+00:00"
  }
}

Nota

  • amount es lo que se descuenta, no lo que recibe el destinatario.
  • Los retiros tienen un límite propio: 5 solicitudes por minuto y 30 por hora. No llames a preview en un bucle.
  • Si el preview venció (expires_at), POST /payouts devuelve 422. Solicita un nuevo preview.

Envía USDT a una billetera externa en TRON

La diferencia es que destination_asset_id también es usdt. En este caso la comisión es mínima.

1
Cotiza el retiro

El activo que se descuenta y el de destino coinciden, así que to_amount es casi amount menos la comisión de red.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-78-preview" \
  -d '{
    "destination_asset_id": "usdt",
    "destination_network": "TRON",
    "destination_address": "TXYZexampletronaddress000000000",
    "amount": "50"
  }'

Puntos clave

destination_network: "TRON"
Toma el valor de network en GET /assets; distingue mayúsculas y minúsculas.
amount: "50"
Se descontarán 50 USDT.
respuesta
{
  "data": {
    "preview_id": "d4e5f6a7-...",
    "from_asset_id": "usdt",
    "from_amount": "50",           // importe a descontar
    "to_asset_id": "usdt",
    "to_amount": "48.9",           // ← importe recibido (después de la comisión de red)
    "fee_amount": "1.1",
    "expires_at": "2026-08-11T17:25:00+00:00"
  }
}
2
Ejecuta el retiro

La misma secuencia que en el escenario anterior.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-78-execute" \
  -d '{
    "preview_id": "d4e5f6a7-...",
    "destination_network": "TRON",
    "destination_address": "TXYZexampletronaddress000000000",
    "external_id": "wd-78"
  }'
respuesta
{
  "data": {
    "payout_id": "payout_01HZZ...",
    "debit_amount": "50",
    "destination_asset_id": "usdt",
    "destination_amount": "48.9",
    "status": "reserved",
    "created_at": "2026-08-11T17:26:00+00:00"
  }
}

Nota

  • Confirma que la dirección del destinatario sea de TRON. Una transferencia a otra red no se ejecuta: la solicitud se rechaza o los fondos no llegarán.

Saldo insuficiente para un retiro

El saldo es de 10 USDT y se solicita un retiro de 15.

1
El preview rechaza la solicitud

El saldo se verifica al cotizar, antes de cualquier descuento. Puedes mostrar el error al usuario de inmediato.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-79-preview" \
  -d '{
    "destination_asset_id": "btc",
    "destination_network": "bitcoin",
    "destination_address": "bc1q…",
    "amount": "15"
  }'

Puntos clave

HTTP 422
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.

solicitud
curl -sS "https://snapex.pro/api/v1/merchant/transactions?limit=50" \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

limit=50
Registros por respuesta: de 1 a 100, 25 por defecto.
status=payment
Filtro opcional por tipo de entrada.
respuesta
{
  "data": [
    {
      "id": "124",
      "direction": "credit",          // acreditación
      "type": "payment",              // origen: depósito
      "amount": "59.4",
      "asset_id": "usdt",
      "channel": "api",
      "occurred_at": "2026-08-11T17:05:00+00:00"
    },
    {
      "id": "125",
      "direction": "reserve",         // reserva para un retiro
      "type": "payout",
      "amount": "15",
      "asset_id": "usdt",
      "occurred_at": "2026-08-11T17:10:00+00:00"
    }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6MTI1fQ",  // ← valor para el parámetro cursor
    "per_page": 50
  }
}
2
Página siguiente

Cuando next_cursor es null, no hay más registros.

solicitud
curl -sS "https://snapex.pro/api/v1/merchant/transactions?limit=50&cursor=eyJpZCI6MTI1fQ" \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Accept: application/json"

Puntos clave

cursor
Valor de meta.next_cursor de la respuesta anterior. No se acepta un valor arbitrario.
respuesta
{
  "data": [ /* más registros */ ],
  "meta": {
    "next_cursor": null,              // ← última página
    "per_page": 50
  }
}

Nota

  • Los valores reserve y release de direction son una retención y una liberación, no una transferencia saliente. No los cuentes como gasto.

Sin respuesta: repite la misma solicitud

1
Repite la misma solicitud

Usa la misma Idempotency-Key y los mismos parámetros. El servidor la trata como la misma operación.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-77-execute" \
  -d '{
    "preview_id": "c3d4e5f6-...",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "external_id": "wd-77"
  }'

Puntos clave

Idempotency-Key
Una clave nueva crea un retiro nuevo.
HTTP 200 / 201
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.

solicitud
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_yourkey" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-77-execute" \
  -d '{
    "preview_id": "another-preview",
    "destination_network": "bitcoin",
    "destination_address": "bc1qanotheraddress"
  }'

Puntos clave

HTTP 409
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

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

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

  3. 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.
solicitud
POST /hooks/snapex HTTP/1.1
Content-Type: application/json
Webhook-Id: 6f1c2a8e-3b4d-4f5a-9c7e-1a2b3c4d5e6f
Webhook-Timestamp: 1790337610
Webhook-Signature: v1=2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
cuerpo
{
  "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

  1. 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.
  2. Calcula el HMAC-SHA256 de «{Webhook-Timestamp}.{raw body}» con tu secreto whsec_… como clave y codifícalo en hex.
  3. Compara «v1=» + hex con el encabezado Webhook-Signature usando una función de tiempo constante: hash_equals, crypto.timingSafeEqual, hmac.compare_digest.
  4. 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);

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