Índice de la documentación
Primeros pasos
API Gateway
Referencia
01 · Primeros pasos
Introducción y conceptos
Qué es OrvenPay, modelo custodial, dirección única por factura, saldos, comisión y fee de red.
OrvenPay es una pasarela de pagos en criptomonedas custodial: tu servidor (web, app o bot) crea una factura por API, el cliente paga en la blockchain y OrvenPay detecta el pago, espera las confirmaciones, acredita el importe a tu saldo (descontando comisión y fee de red) y te avisa con un webhook firmado. Retiras tu saldo cuando quieras a direcciones de tu libreta.
Conceptos clave
- Cuenta: tu usuario en el panel (https://orvenpay.com/dashboard). Los saldos y los retiros son de la cuenta (compartidos por todas sus tiendas).
- Tienda: agrupa API keys, secreto de webhooks, URLs por defecto (
callbackUrl,successUrl,failUrl), monedas activas, quién paga la comisión (feePaidBy) y el fee de red (networkFeePaidBy) y la expiración por defecto (defaultExpireMin, 60 min). Cada tienda tiene plan estándar o White Label. - Factura (
invoice): una orden de cobro con unorderNumberúnico por tienda. Puede tener precio en fiat (sourceCurrency+sourceAmount, el cliente elige la cripto) o en cripto (currency+amount). - Dirección única por factura: cuando se fija la moneda, la plataforma deriva una dirección de depósito NUEVA (HD) sólo para esa factura. El pago se identifica por la dirección, no por un memo. Nunca reutilices una dirección de una factura para otra.
- Pago (
payment): cada transacción on-chain detectada hacia la dirección de la factura. Una factura puede recibir varios pagos (se suman). - Confirmaciones: la factura pasa a
COMPLETEDcuando lo recibido y CONFIRMADO alcanza el importe (con una tolerancia del 0,5 %). Las confirmaciones exigidas dependen de la red (ver Monedas y redes). - Saldo: lo cobrado se acredita en la misma moneda (no hay conversión).
available= disponible,locked= reservado por retiros en curso,withdrawable= retirable ya (los créditos tardan 1 h en "madurar" por seguridad ante reorganizaciones). - Comisión: porcentaje sobre lo cobrado. Por defecto 0.5 % (plan estándar) y 1 % (plan White Label). La paga el comercio (se descuenta del crédito) o el cliente (se suma al importe), según
feePaidByde la tienda. - Fee de red de la factura: cubre el coste real de mover los fondos desde la dirección de depósito (barrido). Lo paga el cliente por defecto (
networkFeePaidBy: CUSTOMER, se suma al importe) o el comercio. Siempre se descuenta del crédito: si lo pagó el cliente, ya venía incluido en lo recibido. - Webhooks: notificaciones HTTP POST firmadas a tu
callbackUrlen cada cambio de estado. Son la fuente de verdad para entregar el pedido.
Flujo completo
Tu servidor OrvenPay API Cliente / blockchain
| POST /v1/invoices | |
|---------------------------->| crea factura (NEW) |
|<----------------------------| { invoice.invoiceUrl } |
| redirige al cliente --------------------------------------->| abre invoiceUrl, elige moneda,
| |<------------------------------| paga a la dirección única
| | detecta tx -> PENDING |
|<--- webhook invoice.pending-| |
| | N confirmaciones -> COMPLETED |
| | acredita saldo (- comisión) |
|<-- webhook invoice.completed| ------------------------------>| redirige a successUrl
| verifica firma, entrega | |
| el pedido, responde 200 | |Convenciones
camelCase en la API nativa (snake_case en la API Gateway). Los importes son strings decimales (nunca floats) y se devuelven normalizados sin ceros finales ("49.90" se devuelve como "49.9"). Las fechas son ISO 8601 en UTC (timestamps Unix en segundos en la API Gateway). Los ids son cadenas opacas alfanuméricas (cuid), p. ej. cmhx3v8qk0004l70gq2b9d7ma.02 · Primeros pasos
Inicio rápido en 5 minutos
Cuenta, tienda, API key, primera factura, redirección y webhook verificado.
- Crea una cuenta en https://orvenpay.com/register y una tienda en Panel → Tiendas → Nueva tienda. Indica la URL de webhooks (
callbackUrl, debe ser pública: no se aceptan localhost ni IPs privadas), las URLs de retorno y las monedas que aceptas. - En la tienda, crea una API key (
op_live_..., se muestra una sola vez) y copia el secreto de webhooks (whsec_...). - Guarda ambos en variables de entorno de tu servidor:
ORVENPAY_API_KEY,ORVENPAY_WEBHOOK_SECRETyORVENPAY_API_URL=https://api.orvenpay.com. - Crea una factura desde tu backend (abajo) y redirige al cliente a
invoice.invoiceUrl. - Implementa el endpoint de webhooks: verifica
X-OrvenPay-Signature, y cuando llegueinvoice.completedmarca el pedido como pagado. Responde200rápido. - Prueba de extremo a extremo con un importe pequeño en una red barata (p. ej.
USDT_BEP20oTRX, mínimo 1 USD). Ver Pruebas.
curl -X POST https://api.orvenpay.com/v1/invoices \
-H "X-Api-Key: $ORVENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"orderNumber": "10245",
"orderName": "Plan Pro mensual",
"sourceCurrency": "USD",
"sourceAmount": "49.90",
"allowedCurrencies": ["USDT_TRC20", "USDT_BEP20", "BTC"],
"successUrl": "https://mitienda.com/pedido/10245/gracias",
"metadata": { "userId": "u_981", "plan": "pro" }
}'Respuesta (201 Created)
{
"invoice": {
"id": "cmhx3v8qk0004l70gq2b9d7ma",
"storeId": "cmhx2a1bc0001l70g5s6t7u8v",
"storeName": null,
"source": "API",
"orderNumber": "10245",
"orderName": "Plan Pro mensual",
"description": null,
"email": "cliente@example.com",
"status": "NEW",
"overpaid": false,
"sourceCurrency": "USD",
"sourceAmount": "49.9",
"currency": null,
"symbol": null,
"network": null,
"rate": null,
"amount": null,
"baseAmount": null,
"amountReceived": "0",
"pendingAmount": null,
"address": null,
"derivationIndex": null,
"confirmations": 0,
"requiredConfirmations": 1,
"allowedCurrencies": ["USDT_TRC20", "USDT_BEP20", "BTC"],
"feePercent": "0.5",
"commission": null,
"feeAmount": "0",
"feeAmountUsd": "0.2495",
"feePaidBy": "MERCHANT",
"networkFee": null,
"networkFeePaidBy": "CUSTOMER",
"networkFeeCharged": "0",
"callbackUrl": "https://mitienda.com/api/orvenpay/webhook",
"successCallbackUrl": null,
"failCallbackUrl": null,
"successUrl": "https://mitienda.com/pedido/10245/gracias",
"failUrl": "https://mitienda.com/pedido/10245",
"metadata": { "userId": "u_981", "plan": "pro" },
"invoiceUrl": "https://orvenpay.com/invoice/cmhx3v8qk0004l70gq2b9d7ma",
"whiteLabel": false,
"expiresAt": "2026-11-08T15:00:00.000Z",
"paidAt": null,
"createdAt": "2026-11-08T14:00:00.000Z",
"updatedAt": "2026-11-08T14:00:00.000Z",
"payments": []
},
"existing": false
}Listo
https://orvenpay.com/llms-full.txt: contiene todo en un único archivo.03 · Primeros pasos
Referencia rápida de endpoints
Todos los endpoints públicos en una tabla: método, ruta, autenticación y propósito.
Especificación completa y tipada: openapi.json. Las rutas del panel (/api/..., sesión de usuario) y las de administración no forman parte de la API pública: no las uses desde integraciones.
04 · Primeros pasos
Autenticación y API keys
X-Api-Key en la API nativa, api_key en la API Gateway, rotación y buenas prácticas.
Cada tienda tiene sus propias API keys con formato op_live_ + 43 caracteres base64url. Envía la clave en la cabecera X-Api-Key en cada petición a /v1:
X-Api-Key: op_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- Se crean en Panel → Tiendas → (tu tienda) → API keys (pide contraseña o 2FA). El valor completo se muestra una sola vez; después sólo se ve el prefijo (12 caracteres) y
lastUsedAt. - Máximo 20 claves activas por tienda. Revocar es inmediato: la siguiente petición con esa clave devuelve
401 INVALID_API_KEY. - La API Gateway acepta la clave como parámetro
api_keyen la query string (lo habitual en plugins), en el cuerpo (application/x-www-form-urlencodedo JSON) o en la cabeceraX-Api-Key. - Alcance: una clave identifica a una tienda (facturas de esa tienda) y a su cuenta (saldos y retiros de la cuenta, compartidos por todas sus tiendas).
- Una clave permite consultar saldos y solicitar retiros (sólo a direcciones ya activadas de tu libreta, sin 2FA): trátala como una credencial financiera.
Rotación de claves
- Crea una clave nueva en el panel.
- Despliega la nueva clave en tus servidores (variable de entorno) y verifica que funciona.
- Revoca la clave antigua.
- API Gateway: los callbacks
verify_hashse firman con la clave que creó cada factura (aunque se revoque después). Mantén la clave antigua en tu verificador hasta que sus facturas abiertas terminen (máximo 24 h de vida + ventana de pagos tardíos).
Secreto de webhooks
Cada tienda tiene un secreto de webhooks (whsec_...) distinto de las API keys. Se ve y se rota en el panel (pide reautenticación). La rotación es inmediata: todos los envíos posteriores, incluidos los reintentos pendientes, se firman con el secreto nuevo. Para rotar sin perder webhooks, acepta durante unos minutos firmas válidas con cualquiera de los dos secretos.
Nunca en el frontend
invoiceUrl. Si una clave se filtra: revócala al momento y revisa Panel → Retiros.05 · Cobros
Facturas
Crear (fiat o cripto), parámetros, importes y redondeo, idempotencia por orderNumber, expiración, metadata, consulta, listado y cancelación.
Crear una factura
/v1/invoicesauth: X-Api-KeyCrea una factura. Cuerpo JSON (Content-Type: application/json, máximo 256 KB). Responde 201 Created con { "invoice": Invoice, "existing": false }, o 200 OK con "existing": true si repites la misma petición (ver idempotencia).
Modos de importe
- Fiat, el cliente elige moneda (recomendado):
sourceCurrency+sourceAmount, sincurrency. La factura nace concurrency: null,amount: null,address: null. Al elegir moneda en la página de pago se fija el tipo de cambio, el importe y una dirección nueva. - Fiat con moneda fijada:
sourceCurrency+sourceAmount+currency. Importe y dirección se calculan al crear. - Cripto:
currency+amount(osourceCurrencycripto +sourceAmount).amountes el importe base en cripto. - Sin ninguno de los modos:
400 AMOUNT_REQUIRED.amountsincurrency:400 CURRENCY_REQUIRED.sourceCurrencycripto distinta decurrency:400 CURRENCY_MISMATCH. Fiat no soportado:400 INVALID_SOURCE_CURRENCY.
Cálculo del importe y redondeo
rate = precio de 1 unidad cripto en sourceCurrency (USD×tipo fiat). En modo cripto: USD por unidad. baseAmount = redondeoARRIBA(sourceAmount / rate, displayDecimals) // o redondeoARRIBA(amount, displayDecimals) comisionCli = feePaidBy == CUSTOMER ? redondeoARRIBA(baseAmount × feePercent / 100, displayDecimals) : 0 feeRedCli = networkFeePaidBy == CUSTOMER ? networkFee : 0 amount = baseAmount + comisionCli + feeRedCli // lo que debe pagar el cliente pendingAmount = max(amount − amountReceived, 0)
displayDecimalspor moneda: BTC/LTC/ETH/BNB 8, TRX 6, stablecoins 4 (ver Monedas). Todo redondeo de importes a cobrar es hacia arriba.networkFee(fee de red de la factura) = max(suelo de la moneda, coste real estimado del barrido × 1,40). Si la estimación tiene más de 30 min, max(suelo, coste × 2).- El tipo de cambio se fija en el momento en que se fija la moneda (al crear o al elegirla) y no cambia durante la vida de la factura.
- La tasa cripto/USD viene de oráculos de mercado; los tipos fiat se actualizan cada hora y se aceptan hasta 6 h. Si no hay tasa fresca:
503 RATES_UNAVAILABLE(reintenta en unos minutos). - Mínimos: el importe base en USD debe ser ≥ el mínimo de la moneda (BTC 5 USD, LTC 1 USD, TRX 1 USD, USDT_TRC20 5 USD, ETH 3 USD, USDT_ERC20 3 USD, USDC_ERC20 3 USD, BNB 1 USD, USDT_BEP20 1 USD) y ≥ 3 × fee de red. Si no:
400 AMOUNT_TOO_SMALLcondetails: { currency, minAmount, minUsd }. Sin moneda fijada basta con que alguna moneda disponible lo cumpla (en la página de pago sólo se ofrecen las que lo cumplen). - Máximo por factura: 10.000.000 USD (
400 AMOUNT_TOO_LARGE).
Idempotencia (orderNumber)
orderNumberes único por tienda. RepetirPOST /v1/invoicescon el mismoorderNumbery los mismos datos de importe (sourceAmount+sourceCurrency,currency,amount) devuelve la factura existente con200 OKy"existing": true(no se crea otra ni se consume otra dirección). Esto hace seguros los reintentos por timeout.- Mismo
orderNumbercon importe o moneda distintos:409 DUPLICATE_ORDER. Si el pedido cambió, usa unorderNumbernuevo (p. ej.10245-2). - La factura devuelta puede estar en cualquier estado (incluso
EXPIRED): compruebastatusantes de redirigir. Para un nuevo intento de pago tras expirar, crea otra factura con otroorderNumber. - Excepción: con
sourceCurrencycripto +sourceAmount, el reintento devuelve409en lugar de la factura existente; para importes en cripto usacurrency+amount. - En la API Gateway el duplicado devuelve el error
107salvo que envíesreturn_existing=1.
Respuesta: objeto Invoice
Objeto Payment
{
"id": "cmhx3w1zt0006l70g0p4n2kxe",
"invoiceId": "cmhx3v8qk0004l70gq2b9d7ma",
"currency": "USDT_TRC20",
"txHash": "5e1c3a9f0b7d24e6c8a1f3b5d7e9c0a2b4d6f8e0a1c3e5b7d9f1a3c5e7b9d1f3",
"logIndex": 0,
"fromAddress": "TBf1Wm7cQ2zR8pLx4NvJ6sD3kHy9aEtU5o",
"toAddress": "TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR",
"amount": "50.91",
"confirmations": 19,
"blockNumber": "67012345",
"status": "CONFIRMED",
"explorerUrl": "https://tronscan.org/#/transaction/5e1c3a9f0b7d24e6c8a1f3b5d7e9c0a2b4d6f8e0a1c3e5b7d9f1a3c5e7b9d1f3",
"createdAt": "2026-11-08T14:08:02.000Z",
"updatedAt": "2026-11-08T14:09:41.000Z"
}En tiendas estándar toAddress es null (igual que address). status de un pago: DETECTED (visto, aún sin confirmaciones suficientes), CONFIRMED (final) u ORPHANED (invalidado por una reorganización de la cadena; no cuenta). blockNumber es string (entero grande).
Expiración
expiresAt = creación + expireMin. La cuenta atrás de la página de pago usaexpiresAty la hora del servidor.- Si al expirar hay un pago detectado aún sin confirmar, la factura sigue
PENDINGhasta 2 h más (desde la expiración o el último pago detectado) esperando confirmaciones. - Pagos tardíos: un pago que llega a una factura
EXPIREDdentro de las 24 h siguientes aexpiresAtse procesa igualmente (la factura puede pasar aCOMPLETEDoMISMATCHy recibirás el webhook correspondiente). - Al expirar sin pago: estado
EXPIREDy webhookinvoice.expired. El cliente es redirigido afailUrldesde la página de pago.
Consultar
/v1/invoices/{id}auth: X-Api-KeyDevuelve { "invoice": Invoice } con payments. 404 NOT_FOUND si no existe o es de otra tienda. Úsalo como respaldo de los webhooks (p. ej. al volver el cliente a successUrl), nunca en bucles agresivos: respeta el rate limit.
Listar y paginar
/v1/invoices?limit=20&cursor=&status=&orderNumber=auth: X-Api-KeyRespuesta: { "items": Invoice[], "nextCursor": string | null }, ordenado por fecha de creación descendente. Repite con cursor=nextCursor hasta que sea null. Los elementos del listado no incluyen payments.
Cancelar
/v1/invoices/{id}/cancelauth: X-Api-KeySólo facturas en NEW (sin pagos detectados). Devuelve { "invoice": Invoice } con status: "CANCELLED" y envía el webhook invoice.cancelled. En otro estado: 409 INVALID_STATUS. Un pago que llegue después a una factura cancelada no se acredita automáticamente (queda para revisión de soporte): cancela sólo si estás seguro de que el cliente no va a pagar.
curl https://api.orvenpay.com/v1/invoices/cmhx3v8qk0004l70gq2b9d7ma -H "X-Api-Key: $ORVENPAY_API_KEY"
curl "https://api.orvenpay.com/v1/invoices?status=COMPLETED,MISMATCH&limit=50" -H "X-Api-Key: $ORVENPAY_API_KEY"
curl -X POST https://api.orvenpay.com/v1/invoices/cmhx3v8qk0004l70gq2b9d7ma/cancel -H "X-Api-Key: $ORVENPAY_API_KEY"06 · Cobros
Página de pago alojada
Qué hace la página de pago alojada (invoiceUrl) y endpoints públicos del checkout (estado, elección de moneda, SSE).
Redirige al cliente a invoiceUrl (https://orvenpay.com/invoice/cmhx3v8qk0004l70gq2b9d7ma). La página:
- Muestra las monedas disponibles con el importe estimado de cada una (sólo las que cumplen el mínimo) si la factura no tiene moneda.
- Muestra importe exacto, red, dirección, QR, desglose (base, comisión y fee de red si los paga el cliente), cuenta atrás y avisos de red (p. ej. "envía sólo USDT por TRC20").
- Se actualiza en tiempo real (Server-Sent Events) con confirmaciones y estado; si el pago parcial está dentro de la tolerancia no pide más al cliente.
- Al completarse redirige a
successUrl; al expirar o cancelarse ofrece volver afailUrl. - Español o inglés según el navegador; tema claro u oscuro.
- El QR codifica BIP21 (
bitcoin:/litecoin:conamount), EIP-681 para ETH/BNB nativos ytron:para TRX. Para tokens (USDT, USDC) el QR lleva sólo la dirección (algunas wallets enviarían al contrato); para tokens EVM hay además un enlace profundo de wallet.
Endpoints públicos del checkout
Base https://api.orvenpay.com/public, sin autenticación y pensados para el navegador del cliente (CORS restringido a los orígenes de la plataforma). Para tu backend usa siempre /v1.
07 · Cobros
White Label
Plan por tienda: datos de pago completos por API (dirección, importe, QR), white_label en /shops y tu marca en el checkout; comisión 1 % por defecto.
White Label es un plan por tienda. Las tiendas estándar pagan la comisión normal (0.5 % por defecto) y su API devuelve sólo la URL de la página de pago alojada. Las tiendas White Label tienen su propia comisión (1 % por defecto) y obtienen los datos completos del pago para mostrarlo dentro de tu web, app o bot, además de tu marca en la página de pago alojada.
- Comisiones vigentes publicadas en
GET https://api.orvenpay.com/public/config→feePercentywhiteLabelFeePercent(pueden existir acuerdos personalizados; la de cada factura está eninvoice.feePercent). - Para mostrar el pago en tu propia interfaz (White Label) fija la moneda al crear (
currency): así la respuesta traeaddressyamount. Si la factura es fiat sin moneda, puedes fijarla después conPOST /public/invoices/{id}/select-currencyy leer de nuevoGET /v1/invoices/{id}. - Muestra siempre importe EXACTO, moneda y red, la dirección con botón de copiar y la cuenta atrás hasta
expiresAt. Recomienda enviar un poco antes de expirar y avisa de que un envío por otra red no se acredita. - Aunque uses tu propia interfaz,
invoiceUrlsigue funcionando como alternativa. - La comisión White Label puede fijarse por tienda (override acordado con la plataforma); la aplicada a cada factura queda en
invoice.feePercenty eninvoice_commission_percentagedeGET /gateway/api/v1/currencies. - Los webhooks (OrvenPay y callbacks de la API Gateway) son iguales en ambos planes.
{
"invoice": {
"id": "cmhx4b2rn0009l70g8w1c5e3ds",
"storeId": "cmhx2a1bc0001l70g5s6t7u8v",
"storeName": null,
"source": "API",
"orderNumber": "10246",
"orderName": "Plan Pro mensual",
"description": null,
"email": null,
"status": "NEW",
"overpaid": false,
"sourceCurrency": "USD",
"sourceAmount": "49.9",
"currency": "USDT_TRC20",
"symbol": "USDT",
"network": "TRC20",
"rate": "0.9998",
"amount": "50.91",
"baseAmount": "49.91",
"amountReceived": "0",
"pendingAmount": "50.91",
"address": "TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR",
"derivationIndex": 1532,
"confirmations": 0,
"requiredConfirmations": 19,
"allowedCurrencies": [],
"feePercent": "1",
"commission": "0.4991",
"feeAmount": "0",
"feeAmountUsd": "0.499",
"feePaidBy": "MERCHANT",
"networkFee": "1",
"networkFeePaidBy": "CUSTOMER",
"networkFeeCharged": "0",
"callbackUrl": null,
"successCallbackUrl": null,
"failCallbackUrl": null,
"successUrl": null,
"failUrl": null,
"metadata": null,
"invoiceUrl": "https://orvenpay.com/invoice/cmhx4b2rn0009l70g8w1c5e3ds",
"whiteLabel": true,
"expiresAt": "2026-11-08T15:00:00.000Z",
"paidAt": null,
"createdAt": "2026-11-08T14:00:00.000Z",
"updatedAt": "2026-11-08T14:00:00.000Z",
"payments": []
},
"existing": false
}Cómo activarlo
08 · Cobros
Estados y ciclo de vida
Máquina de estados de la factura, eventos, tolerancia de pago insuficiente, sobrepagos, pagos tardíos y casos límite.
pago detectado confirmado >= amount×(1−0,5%)
NEW ───────────────────────────────> PENDING ─────────────────────────────> COMPLETED (final, acreditada)
│ \ │ \ expira con pago parcial
│ \ (pago ya confirmado al verse) │ \ confirmado (tras 2 h de gracia
│ └──────────────────────────────────┼─────> si quedaban pagos sin confirmar)
│ │ MISMATCH (final, acreditada)
│ expira sin pagos │ todos los pagos invalidados
▼ ▼ por reorganización
EXPIRED <──────────────────────────────── NEW / EXPIRED
│ pago tardío (≤ 24 h tras expiresAt)
└──────────────> COMPLETED o MISMATCH
NEW ──(cancelar por API/panel)──> CANCELLED (final) ERROR (final, incidencia interna: contacta soporte)- No asumas el orden: si un pago se detecta ya confirmado, la factura puede pasar de
NEWaCOMPLETEDsininvoice.pending. Si un pago se invalida por una reorganización, una facturaPENDINGpuede volver aNEW(sin webhook). - Tolerancia de pago insuficiente: 0.5 %. Con lo confirmado ≥
amount × (1 − 0,005)la factura se completa (la página pública lo expone comominAcceptedAmount). - Sobrepago: si lo recibido supera
amount × 1,005, la factura se completa conoverpaid: true. Todo lo recibido y confirmado hasta completar se acredita. - Varios pagos: se suman. Un pago que se confirme DESPUÉS de que la factura sea final (
COMPLETED/MISMATCH) no se acredita automáticamente: queda registrado para revisión de soporte. - Red o moneda equivocada (p. ej. USDT por ERC20 a una factura TRC20, o TRX nativo a una factura USDT_TRC20): no se acredita automáticamente; queda como pago huérfano para revisión de soporte.
- Los estados finales no cambian salvo
EXPIREDcon un pago tardío. TrataCOMPLETEDyMISMATCHcomo "dinero acreditado".
09 · Cobros
Webhooks
Eventos, payload, cabeceras, verificación HMAC-SHA256 (Node.js, Python, PHP, Go), cuerpo crudo, reintentos, orden, idempotencia y protección anti-replay.
Cuando una factura creada por la API nativa (o desde el panel) cambia de estado, OrvenPay envía un POST JSON a su callbackUrl (o a la callbackUrl de la tienda si la factura no tiene una). Las facturas creadas por la API Gateway usan otro formato (callbacks con verify_hash, ver API Gateway). Sin URL configurada no se envía nada.
Petición
POST /api/orvenpay/webhook HTTP/1.1
Host: mitienda.com
Content-Type: application/json
User-Agent: OrvenPay-Webhooks/1.0
X-OrvenPay-Event: invoice.completed
X-OrvenPay-Delivery: cmhx3x9ab0007l70gh8j2k4lm
X-OrvenPay-Signature: t=1794146981,v1=9c1f0e5a7b3d2c4e6f8a0b1c3d5e7f9a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d0e
{
"event": "invoice.completed",
"createdAt": "2026-11-08T14:09:41.000Z",
"data": {
"id": "cmhx3v8qk0004l70gq2b9d7ma",
"storeId": "cmhx2a1bc0001l70g5s6t7u8v",
"orderNumber": "10245",
"orderName": "Plan Pro mensual",
"status": "COMPLETED",
"overpaid": false,
"currency": "USDT_TRC20",
"network": "TRC20",
"amount": "50.91",
"amountReceived": "50.91",
"feeAmount": "0.25455",
"address": "TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR",
"sourceCurrency": "USD",
"sourceAmount": "49.9",
"rate": "0.9998",
"confirmations": 19,
"requiredConfirmations": 19,
"metadata": { "userId": "u_981", "plan": "pro" },
"paidAt": "2026-11-08T14:09:41.000Z",
"expiresAt": "2026-11-08T15:00:00.000Z"
}
}Verificar la firma
- Lee el cuerpo crudo exactamente como llegó (bytes/string), ANTES de parsear JSON. Re-serializar el JSON cambia los bytes y rompe la firma.
- Separa la cabecera
X-OrvenPay-Signaturepor comas en paresclave=valor; obténtyv1. - Rechaza si
|ahora − t| > 300segundos (protección anti-replay; sincroniza el reloj con NTP). - Calcula
HMAC-SHA256(secreto, t + "." + cuerpo_crudo)en hex y compáralo conv1en tiempo constante. - Si no coincide responde
400/401y no proceses nada.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.ORVENPAY_WEBHOOK_SECRET; // whsec_...
export function verifyOrvenPaySignature(rawBody, header, secret = SECRET, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 || '')) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
}
// express.raw: el cuerpo llega como Buffer SIN parsear (imprescindible)
app.post('/api/orvenpay/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyOrvenPaySignature(raw, req.get('X-OrvenPay-Signature'))) return res.status(400).send('bad signature');
const evt = JSON.parse(raw);
// Idempotencia: procesa cada (factura, evento) una sola vez
if (evt.event === 'invoice.completed') {
await markOrderPaid(evt.data.orderNumber, evt.data.id, evt.data.amountReceived, evt.data.currency);
}
res.sendStatus(200); // responde en < 10 s; tareas largas, a una cola
});El error más común: el cuerpo ya parseado
express.json(), Next.js con req.json(), Laravel con $request->all() o FastAPI con un modelo Pydantic te dan el JSON ya parseado. Firma SIEMPRE sobre el cuerpo crudo: express.raw(), await req.text(), $request->getContent() / php://input, await request.body().Reintentos, timeouts y orden
- Se considera entregado con cualquier respuesta 2xx recibida en menos de 10 s. Cualquier otra cosa (3xx, 4xx, 5xx, timeout, error de red/TLS) es un fallo. No se siguen redirecciones: usa la URL final.
- Hasta 8 intentos por entrega durante unas 43 h; después queda
FAILEDy puedes reenviarla desde Panel → Facturas → (factura) → Webhooks. - Destinos internos (IPs privadas, localhost) se bloquean y fallan de forma permanente (protección SSRF): la URL debe resolver a una IP pública.
- Orden garantizado por factura y URL: el siguiente evento de una factura no se envía hasta que el anterior se entregue o falle definitivamente. Así nunca recibes
expiredantes quepending. - Entregas al menos una vez: puedes recibir el mismo evento más de una vez (reintento tras un timeout tuyo, reenvío manual). Diseña el manejador idempotente.
Manejo idempotente recomendado
- Verifica la firma y el timestamp.
- Busca tu pedido por
data.orderNumber(o guardadata.idal crear la factura y busca por él). Ignora facturas que no reconozcas (responde 200). - Aplica la transición sólo si avanza: guarda en tu BD el estado procesado por factura; si ya procesaste ese
(data.id, event)responde 200 sin repetir efectos. - Entrega el pedido SÓLO con
invoice.completed(y decide qué hacer coninvoice.mismatch). Comprueba, si quieres, quedata.currency/data.amountReceivedson coherentes con tu pedido. - Responde 200 en < 10 s y procesa tareas lentas (emails, provisión) en segundo plano.
- Opcional: ante cualquier duda, confirma con
GET /v1/invoices/{id}(fuente de verdad).
10 · API Gateway
API Gateway
Capa compatible con plugins y SDK de pasarelas (api_key + verify_hash HMAC-SHA1): endpoints, parámetros, respuestas estándar/White Label, callbacks, verificación y códigos de error.
Además de la API v1, OrvenPay expone la API Gateway: el formato api_key + verify_hash (HMAC-SHA1) que usan muchos plugins de e-commerce (WooCommerce, PrestaShop, WHMCS...) y SDK de pasarelas cripto. Configura en tu plugin la URL base https://api.orvenpay.com/gateway y una API key de tu tienda; el plugin llamará a https://api.orvenpay.com/gateway/api/v1/.... Para integraciones nuevas recomendamos la API nativa (/v1).
- URL base para plugins/SDK:
https://api.orvenpay.com/gateway. Endpoints reales:https://api.orvenpay.com/gateway/api/v1/<recurso>. - Autenticación:
api_keyen la query string (habitual), en el cuerpo (form o JSON) o cabeceraX-Api-Key. Misma API key que la API nativa. - Parámetros: se aceptan en la query string y/o en el cuerpo (
application/x-www-form-urlencodedo JSON); si un parámetro está en ambos, gana el cuerpo. - Respuesta:
{ "status": "success", "data": {...} }. Importes como string, timestamps Unix (segundos) como número. - Error:
{ "status": "error", "data": { "name", "message", "code" } }(ver tabla de códigos abajo). En errores de validación,messagees un JSON con{"campo":"motivo"}(los SDK PHP hacenjson_decode(message)). - Rate limit: 120 peticiones/min por API key (429 con
Retry-After). - Las facturas creadas aquí tienen
source: "GATEWAY"y notifican con callbacksverify_hash(no con webhooks OrvenPay).
Crear factura: invoices/new
/gateway/api/v1/invoices/newauth: api_keycurl "https://api.orvenpay.com/gateway/api/v1/invoices/new?api_key=$ORVENPAY_API_KEY&order_number=10245&order_name=Plan%20Pro&source_currency=USD&source_amount=49.90&callback_url=https%3A%2F%2Fmitienda.com%2Fcallback.php%3Fjson%3Dtrue"Respuesta en tiendas estándar
Sólo txn_id (id de la factura), invoice_url (página de pago alojada) e invoice_total_sum (total en cripto; cadena vacía si el cliente aún no eligió moneda). Redirige al cliente a invoice_url.
{
"status": "success",
"data": {
"txn_id": "cmhx3v8qk0004l70gq2b9d7ma",
"invoice_url": "https://orvenpay.com/invoice/cmhx3v8qk0004l70gq2b9d7ma",
"invoice_total_sum": ""
}
}Respuesta en tiendas White Label
Datos completos para mostrar el pago dentro de tu web o bot. Envía currency/psys_cid para recibir dirección e importe (sin moneda, wallet_hash, amount y qr_code llegan vacíos). qr_code es un PNG en data URI; source_rate = unidades de cripto por 1 unidad de source_currency; expire_utc es Unix en segundos. verify_hash se calcula sobre el resto de campos de data con el mismo algoritmo que los callbacks en modo formulario.
{
"status": "success",
"data": {
"txn_id": "cmhx4b2rn0009l70g8w1c5e3ds",
"invoice_url": "https://orvenpay.com/invoice/cmhx4b2rn0009l70g8w1c5e3ds",
"amount": "50.91",
"pending_amount": "50.91",
"wallet_hash": "TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR",
"psys_cid": "USDT_TRX",
"currency": "USDT_TRX",
"status": "new",
"source_currency": "USD",
"source_rate": "1.000200",
"expire_utc": 1794150000,
"expected_confirmations": "19",
"qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"verify_hash": "2b7d0c4e9a1f3e5d7c9b0a2f4e6d8c1b3a5f7e9d",
"invoice_commission": "1",
"invoice_sum": "49.91",
"invoice_total_sum": "50.91"
}
}Callbacks y verify_hash
callback_url(o la URL de webhooks de la tienda) recibe cada cambio de estado:pending,completed,mismatch,expired,cancelled.success_callback_urlrecibe ademáscompleted;fail_callback_urlrecibe ademásexpired,cancelledy el pago parcial expirado.- Formato:
application/x-www-form-urlencodedpor defecto; JSON si la URL contienejson=true. Todos los valores son strings y las claves van ordenadas comoksort()de PHP, converify_hashal final. - Firma:
verify_hash = HMAC-SHA1(clave, mensaje)en hex, donde la clave es la API key que creó la factura (si no la hay, el secreto de webhooks de la tienda) y el mensaje es: en formulario,serialize()de PHP del array ordenado sinverify_hash; en JSON,JSON.stringifydel objeto sinverify_hashcon las claves ordenadas y sin espacios. - Mismas reglas de entrega que los webhooks OrvenPay: 2xx en < 10 s, 8 intentos con backoff, orden por factura, sin seguir redirecciones. También llevan
X-OrvenPay-EventyX-OrvenPay-Delivery. comment:"overpaid"en sobrepago,"underpaid"en pago parcial expirado, vacío en el resto.
POST /callback.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
User-Agent: OrvenPay-Webhooks/1.0
X-OrvenPay-Event: invoice.completed
X-OrvenPay-Delivery: cmhx3x9ab0007l70gh8j2k4lm
amount=50.91&comment=&confirmations=19¤cy=USDT_TRX&invoice_commission=1&invoice_sum=49.91&invoice_total_sum=50.91&ipn_type=invoice&merchant=Mi+tienda&merchant_id=cmhx2a1bc0001l70g5s6t7u8v&order_name=Plan+Pro+mensual&order_number=10245&pending_amount=0&psys_cid=USDT_TRX&source_amount=49.9&source_currency=USD&source_rate=1.000200&status=completed&txn_id=cmhx3v8qk0004l70gq2b9d7ma&wallet_hash=TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR&verify_hash=8f3a...e21c{
"amount": "50.91",
"comment": "",
"confirmations": "19",
"currency": "USDT_TRX",
"invoice_commission": "1",
"invoice_sum": "49.91",
"invoice_total_sum": "50.91",
"ipn_type": "invoice",
"merchant": "Mi tienda",
"merchant_id": "cmhx2a1bc0001l70g5s6t7u8v",
"order_name": "Plan Pro mensual",
"order_number": "10245",
"pending_amount": "0",
"psys_cid": "USDT_TRX",
"source_amount": "49.9",
"source_currency": "USD",
"source_rate": "1.000200",
"status": "completed",
"txn_id": "cmhx3v8qk0004l70gq2b9d7ma",
"wallet_hash": "TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR",
"verify_hash": "c47e0b2d9f8a1e3c5b7d9f0a2c4e6b8d1f3a5c7e"
}<?php
function verify_callback(array $post, string $apiKey): bool {
if (!isset($post['verify_hash'])) { return false; }
$hash = $post['verify_hash'];
unset($post['verify_hash']);
ksort($post);
$expected = hash_hmac('sha1', serialize($post), $apiKey);
return hash_equals($expected, $hash);
}
if (!verify_callback($_POST, getenv('ORVENPAY_API_KEY'))) {
http_response_code(400);
exit;
}
if ($_POST['status'] === 'completed' || $_POST['status'] === 'mismatch') {
// pagado (mismatch = sobrepago); localiza el pedido por $_POST['order_number']
}
http_response_code(200);Detalles que rompen la verificación
serialize() son en bytes UTF-8 (no caracteres). Conserva los campos vacíos (comment=). En JSON no uses espacios (separators=(",", ":") en Python) ni escapes / o Unicode (JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE en PHP). Usa la API key con la que creaste la factura.Estados en formato Gateway
Operaciones
/gateway/api/v1/operations/{id}auth: api_keyUna factura (type: "invoice") o un retiro (type: "cash_out") por id. 404 con código 111 si no existe o no es tuyo. En tiendas estándar las facturas llegan con wallet_hash: "" (sin dirección de depósito). Extras útiles en facturas: txn_id, invoice_url, invoice_total_sum, pending_amount, overpaid; en retiros: txn_id, internal_status, error.
{
"status": "success",
"data": {
"user_id": "cmhx29zz00000l70gxk1m2n3o",
"shop_id": "cmhx2a1bc0001l70g5s6t7u8v",
"type": "invoice",
"status": "completed",
"pending_sum": "0",
"psys_cid": "USDT_TRX",
"currency": "USDT_TRX",
"source_currency": "USD",
"source_rate": "1.000200",
"fee": null,
"wallet_hash": "TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR",
"sendmany": null,
"params": {
"order_number": "10245",
"order_name": "Plan Pro mensual",
"description": "",
"email": "",
"amount": "49.91",
"currency": "USDT_TRX",
"psys_cid": "USDT_TRX",
"source_amount": "49.9",
"source_currency": "USD",
"source_rate": "1.000200",
"callback_url": "https://mitienda.com/callback.php",
"success_callback_url": "",
"fail_callback_url": "",
"success_invoice_url": "",
"fail_invoice_url": ""
},
"expire_at_utc": 1794150000,
"created_at_utc": 1794146400,
"finished_at_utc": 1794146981,
"amount": "50.91",
"sum": "50.91",
"commission": "1",
"tx_url": ["https://tronscan.org/#/transaction/5e1c3a9f0b7d24e6c8a1f3b5d7e9c0a2b4d6f8e0a1c3e5b7d9f1a3c5e7b9d1f3"],
"tx_id": ["5e1c3a9f0b7d24e6c8a1f3b5d7e9c0a2b4d6f8e0a1c3e5b7d9f1a3c5e7b9d1f3"],
"tx": [
{
"txid": "5e1c3a9f0b7d24e6c8a1f3b5d7e9c0a2b4d6f8e0a1c3e5b7d9f1a3c5e7b9d1f3",
"value": "50.91",
"amount": "50.91",
"confirmations": 19,
"block": 67012345,
"processed": true,
"status": "confirmed",
"url": "https://tronscan.org/#/transaction/5e1c3a9f0b7d24e6c8a1f3b5d7e9c0a2b4d6f8e0a1c3e5b7d9f1a3c5e7b9d1f3"
}
],
"id": "cmhx3v8qk0004l70gq2b9d7ma",
"actual_sum": "50.91",
"actual_commission": "0.25455",
"actual_fee": "1",
"actual_invoice_sum": "49.65545",
"confirmations": 19,
"expected_confirmations": 19,
"parent_id": null,
"child_ids": [],
"txn_id": "cmhx3v8qk0004l70gq2b9d7ma",
"invoice_url": "https://orvenpay.com/invoice/cmhx3v8qk0004l70gq2b9d7ma",
"invoice_total_sum": "50.91",
"pending_amount": "0",
"overpaid": false
}
}/gateway/api/v1/operationsauth: api_keyRespuesta: data.operations[], data._links (self, first, last, prev, next con href) y data._meta (totalCount, pageCount, currentPage, perPage). Se combinan facturas de la tienda y retiros de la cuenta ordenados por fecha; las páginas más allá de 500 filas por fuente vuelven vacías.
Saldos, monedas y tienda
Retiros por la API Gateway
/gateway/api/v1/operations/withdrawauth: api_key- Desactivado por defecto por tienda (
403, código119); pide a soporte que lo habilite si lo necesitas. La API nativaPOST /v1/withdrawalsno requiere esta habilitación. - Parámetros:
currencyopsys_cid,to(dirección activa de tu libreta),amount(decimal),type=cash_out(por defecto).mass_cash_outy destinos separados por comas no se admiten (103).feePlan/fee_plan/feeRatese aceptan e ignoran. - Idempotencia: la misma petición (tienda, moneda, destino, importe) repetida en 120 s devuelve la misma operación con
200; un retiro nuevo responde201. - Se aplican todas las reglas de retiro (mínimos, fee, límite diario, revisión, maduración, pausa). Respuesta: una operación
type: "cash_out"constatuspending/completed/error/cancelled.
curl "https://api.orvenpay.com/gateway/api/v1/balances/USDT_TRX?api_key=$ORVENPAY_API_KEY"
curl "https://api.orvenpay.com/gateway/api/v1/currencies/EUR?api_key=$ORVENPAY_API_KEY"
curl "https://api.orvenpay.com/gateway/api/v1/shops?api_key=$ORVENPAY_API_KEY"
curl "https://api.orvenpay.com/gateway/api/v1/operations?api_key=$ORVENPAY_API_KEY&type=invoice&status=completed&page=1&limit=50"
curl "https://api.orvenpay.com/gateway/api/v1/operations/cmhx3v8qk0004l70gq2b9d7ma?api_key=$ORVENPAY_API_KEY"
curl "https://api.orvenpay.com/gateway/api/v1/operations/withdraw?api_key=$ORVENPAY_API_KEY&psys_cid=USDT_TRX&to=TXk9vQ3mPz7Lr2WbN8cDs5HfYuJ4aEgR6t&amount=250&type=cash_out"Identificadores de moneda (psys_cid)
Errores de la API Gateway
HTTP/1.1 400 Bad Request
{
"status": "error",
"data": {
"name": "Bad Request",
"message": "{\"order_number\":\"Missing required attribute\"}",
"code": 103
}
}Cualquier otro error usa el estado HTTP como data.code (p. ej. 409 para INVALID_STATUS) o 108 si es 5xx. data.name es el texto HTTP (Bad Request, Unauthorized, Not Found, Unprocessable entity, Too Many Requests...).
Puesta en marcha con un plugin
- Crea tu cuenta y una tienda, y activa las monedas que aceptas.
- En el plugin, configura la URL base (API URL / endpoint)
https://api.orvenpay.com/gatewayy la API key de la tienda (campo "API key" / "secret key"). - Comprueba que el plugin usa la URL de callback de tu tienda (o fija la URL de webhooks en la tienda).
- Si haces retiros por API, añade antes los destinos a tu libreta: se activan tras el periodo de espera.
- Crea un pedido de prueba de importe pequeño y comprueba que el callback valida
verify_hashy marca el pedido como pagado.
11 · Fondos
Saldos y retiros
Saldos por moneda, maduración de créditos, libreta de direcciones con activación, mínimos, fees, revisión, límite diario, pausa e idempotencia de retiros.
Saldos
/v1/balancesauth: X-Api-Key- Saldos de la cuenta (todas sus tiendas), una fila por moneda activa o con saldo.
available: disponible.locked: reservado por retiros en curso (importe + fee).withdrawable: retirable ahora (=availablemenos créditos aún en maduración).maturingUntil: cuándo madura el último crédito pendiente.availableUsd: valor orientativo.- Maduración: cada crédito de factura es retirable 1 h después de acreditarse (protección ante reorganizaciones de la cadena).
withdrawalsPaused: truesignifica que la plataforma ha pausado los retiros temporalmente (los fondos siguen seguros).
{
"items": [
{
"currency": "USDT_TRC20",
"name": "Tether USD",
"symbol": "USDT",
"network": "TRC20",
"icon": "usdt",
"networkIcon": "trx",
"available": "1520.4471",
"locked": "251",
"withdrawable": "1469.5371",
"maturingUntil": "2026-11-08T15:09:41.000Z",
"availableUsd": "1520.14"
},
{
"currency": "BTC",
"name": "Bitcoin",
"symbol": "BTC",
"network": "Bitcoin",
"icon": "btc",
"networkIcon": null,
"available": "0",
"locked": "0",
"withdrawable": "0",
"maturingUntil": null,
"availableUsd": "0.00"
}
],
"totalUsd": "1520.14",
"withdrawalsPaused": false
}Libreta de direcciones
- Los retiros sólo pueden ir a direcciones de tu libreta (Panel → Retiros → Libreta). Añadir una dirección exige contraseña o 2FA y se valida el formato de la red; no se admiten direcciones de la propia plataforma.
- Cada dirección nueva se activa 24 h después de añadirla (
activatesAt). Antes, un retiro hacia ella da403 ADDRESS_NOT_ACTIVEcondetails.activatesAt. - La libreta y el auto-retiro por moneda (dirección + importe mínimo, por tienda) se gestionan sólo desde el panel; la API no puede añadir destinos (así una clave filtrada no puede enviar fondos a una dirección nueva).
Crear un retiro
/v1/withdrawalsauth: X-Api-KeyRate limit: 20/min por clavecurl -X POST https://api.orvenpay.com/v1/withdrawals \
-H "X-Api-Key: $ORVENPAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: payout-2026-11-08-001" \
-d '{ "currency": "USDT_TRC20", "address": "TXk9vQ3mPz7Lr2WbN8cDs5HfYuJ4aEgR6t", "amount": "250" }'
curl https://api.orvenpay.com/v1/withdrawals/cmhx9k2lm0011l70gr3s5t7uv -H "X-Api-Key: $ORVENPAY_API_KEY"Mínimos y fees de retiro
La plataforma no cobra % por retirar: sólo el fee de red publicado por moneda. Se cobra max(tabla, coste real estimado × (1 + 10 %)), así que en congestión extrema puede ser mayor que la tabla (consulta el vigente en GET /v1/currencies → withdrawalFee). Valores por defecto:
Reglas y errores de retiro
12 · Fondos
Monedas y redes
Monedas soportadas con red, contrato, confirmaciones, decimales, mínimos y fee de red; monedas fiat de precio.
Usa el código en currency/allowedCurrencies (la API nativa acepta también el psysCid). La disponibilidad real depende de la configuración de la plataforma y de tu tienda: consúltala en GET /v1/currencies (available).
- Los tokens (USDT, USDC) sólo se acreditan si llegan por la red y el contrato indicados. Un envío por otra red o un token distinto queda como pago huérfano para soporte.
- Las direcciones EVM son iguales en Ethereum y BNB Smart Chain: un pago por la cadena equivocada NO se acredita automáticamente. Muestra siempre la red al cliente.
- Ningún token requiere memo/tag: cada factura tiene su propia dirección.
- Monedas fiat para
sourceCurrency: USD, EUR, GBP, RUB, UAH, BRL, MXN, ARS, COP, CLP, PEN, VES, TRY, INR, CNY, JPY, KRW, CAD, AUD, CHF, PLN, NGN, KZT, IDR, PHP, VND, THB, AED, SAR. - Monedas activas por defecto: BTC, LTC, TRX, USDT_TRC20, ETH, USDT_ERC20, USDC_ERC20, BNB, USDT_BEP20.
/v1/currenciesauth: X-Api-Key{
"items": [
{
"code": "USDT_TRC20",
"psysCid": "USDT_TRX",
"name": "Tether USD",
"symbol": "USDT",
"icon": "usdt",
"networkIcon": "trx",
"network": "TRC20",
"decimals": 6,
"displayDecimals": 4,
"confirmations": 19,
"rateUsd": "0.9998",
"withdrawalFee": "1",
"available": true
}
]
}rateUsd es el precio actual en USD (null si no hay tasa fresca). withdrawalFee es el fee de retiro publicado. Total de monedas soportadas: 9. Página de pago y monedas activas públicas: GET https://api.orvenpay.com/public/config.
13 · Fondos
Comisiones y mínimos
Comisión por plan (0,5 % estándar, 1 % White Label), quién paga comisión y fee de red, cómo se descuenta del crédito y ejemplos numéricos.
recibido = suma de pagos confirmados
comision = feePaidBy == MERCHANT ? redondeoARRIBA(recibido × feePercent / 100, decimales on-chain)
: amount − baseAmount − (feeRed si lo pagó el cliente)
feeRed = networkFee de la factura (SIEMPRE se descuenta; si lo pagó el cliente ya venía dentro de "recibido")
crédito neto = recibido − comision − feeRed (nunca negativo: si no alcanza, se cubre primero el fee de red)rate = 0.9998 USD/USDT baseAmount = redondeoARRIBA(49.90 / 0.9998, 4) = 49.9100 USDT amount = 49.91 + 1 (fee de red) = 50.91 USDT <- lo que paga el cliente comisión = 50.91 × 0.5 % = 0.25455 USDT crédito = 50.91 − 0.25455 − 1 = 49.65545 USDT <- lo que entra en tu saldo
- La comisión de cada factura queda fijada en
invoice.feePercental crearla; la cobrada real, enfeeAmount(yfeeAmountdel webhook). - Mínimos de factura y fees de red por moneda: ver Monedas y redes. Regla: base ≥ mínimo USD de la moneda y ≥ 3 × fee de red.
- Comisiones vigentes publicadas:
GET /public/config→feePercent,whiteLabelFeePercent. La plataforma puede pactar comisiones personalizadas por cuenta.
14 · Referencia
Límites y rate limits
Peticiones por minuto por API key e IP, cupo de facturas por cuenta, tamaños máximos y cabeceras de rate limit.
- Al superar un límite:
429 RATE_LIMITEDcon cabeceraRetry-After(segundos). Las respuestas incluyenx-ratelimit-limit,x-ratelimit-remainingyx-ratelimit-reset. - Reintenta con backoff exponencial y jitter (p. ej. 1 s, 2 s, 4 s... máximo 60 s). No reintentes 4xx distintos de 429/409 sin corregir la petición.
- Si el almacén de rate limit no está disponible, las rutas de API responden
503 SERVICE_UNAVAILABLE(conRetry-After: 5): reintenta. - Timeouts recomendados en tu cliente: 15 s para crear facturas o retiros. Ante un timeout, repite la MISMA petición (idempotente por
orderNumber/Idempotency-Key).
15 · Referencia
Errores
Formato de error y catálogo completo de códigos de la API nativa y pública.
Todas las respuestas de error de la API nativa y pública tienen este formato (la API Gateway usa el suyo, ver API Gateway). Decide por code (estable), no por message (texto en español que puede cambiar). details aparece en algunos errores.
{
"error": {
"code": "AMOUNT_TOO_SMALL",
"message": "Importe demasiado pequeño para USDT_TRC20: mínimo 5.0011 USDT (5 USD)",
"details": { "currency": "USDT_TRC20", "minAmount": "5.0011", "minUsd": "5" }
}
}- Reintentables:
409 CONFLICT,409 TRANSACTION_CONFLICT,429,500,503. Usa siempre la misma clave de idempotencia (orderNumber,Idempotency-Key). - No reintentables sin cambios: el resto de 4xx.
16 · Referencia
Seguridad
Buenas prácticas: secretos, verificación de webhooks, no confiar en el navegador, importes, idempotencia y respuesta ante incidentes.
- Secretos en el servidor: API key y secreto de webhooks sólo en variables de entorno o gestor de secretos; nunca en el cliente, repositorios, logs ni URLs que compartas. En la API Gateway la clave viaja en la query: no registres URLs completas en tus logs.
- Una clave por entorno/servicio y revoca las que no uses. Rota periódicamente (ver Autenticación).
- Verifica SIEMPRE la firma del webhook (HMAC-SHA256 en tiempo constante + ventana de 5 min) o el
verify_hashdel callback Gateway antes de tocar un pedido. - No confíes en la redirección: llegar a
successUrlno prueba el pago. Entrega sólo coninvoice.completedverificado (o tras consultarGET /v1/invoices/{id}desde tu servidor). - Comprueba la coherencia: que
data.orderNumberexiste, que la moneda/importe corresponden a tu pedido y que no lo has procesado ya. - HTTPS en tus URLs de webhook y retorno. Las URLs de webhook deben ser públicas: se bloquean IPs privadas y se revalida la IP en cada conexión (anti DNS-rebinding).
- Responde rápido y procesa en cola: un endpoint lento provoca reintentos duplicados.
- Protege los retiros: activa 2FA en tu cuenta, mantén una libreta mínima y revisa los avisos. Las direcciones nuevas tardan en activarse precisamente para darte margen si alguien roba tu sesión o tu clave.
- Custodia: los fondos están bajo custodia de OrvenPay hasta que los retiras; usa el auto-retiro por moneda si prefieres no mantener saldo.
- Incidente: si sospechas de una filtración, revoca la clave, rota el secreto de webhooks, cambia la contraseña y contacta con soporte para congelar retiros.
17 · Guías
Recetas de integración
Código completo listo para copiar: bots de Telegram (python-telegram-bot, grammY), Next.js, Express, PHP/Laravel y plugins de e-commerce vía API Gateway.
Todas las recetas siguen el mismo patrón: (1) crear la factura en el servidor con POST /v1/invoices, (2) enviar al cliente a invoiceUrl, (3) verificar el webhook y entregar con invoice.completed. Variables de entorno: ORVENPAY_API_URL=https://api.orvenpay.com, ORVENPAY_API_KEY, ORVENPAY_WEBHOOK_SECRET.
Bots de Telegram
- El bot crea la factura al recibir un comando o botón y envía
invoiceUrlcomo botón de URL (el cliente paga en el navegador; con White Label puedes enviar dirección e importe en el chat). - Guarda el
chat_idenmetadata({"chatId": 123}): vuelve en cada webhook y te permite avisar al usuario sin consultar tu BD. - El webhook de OrvenPay necesita una URL HTTPS pública: corre un pequeño servidor HTTP junto al bot (aunque el bot use polling).
- Un
orderNumberpor intento (tg-<chat>-<timestamp>); si quieres reutilizar la factura de un pedido pendiente, usa el mismoorderNumbery mismo importe (idempotente).
# pip install "python-telegram-bot>=21" aiohttp httpx
import asyncio, hashlib, hmac, json, os, time
import httpx
from aiohttp import web
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CommandHandler, ContextTypes
API = os.environ["ORVENPAY_API_URL"]
API_KEY = os.environ["ORVENPAY_API_KEY"]
SECRET = os.environ["ORVENPAY_WEBHOOK_SECRET"].encode()
processed: set[str] = set() # en producción: tabla en tu BD
async def comprar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat_id = update.effective_chat.id
async with httpx.AsyncClient(timeout=15) as http:
r = await http.post(
f"{API}/v1/invoices",
headers={"X-Api-Key": API_KEY},
json={
"orderNumber": f"tg-{chat_id}-{int(time.time())}",
"orderName": "Premium 30 días",
"sourceCurrency": "USD",
"sourceAmount": "9.99",
"allowedCurrencies": ["USDT_TRC20", "USDT_BEP20", "TRX", "LTC"],
"metadata": {"chatId": chat_id},
},
)
body = r.json()
if r.status_code >= 400:
await update.message.reply_text("No se pudo crear el pago: " + body["error"]["message"])
return
kb = InlineKeyboardMarkup([[InlineKeyboardButton("Pagar con cripto", url=body["invoice"]["invoiceUrl"])]])
await update.message.reply_text("Tu factura está lista (válida 60 min).", reply_markup=kb)
def verify(raw: bytes, header: str) -> bool:
try:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > 300:
return False
expected = hmac.new(SECRET, str(t).encode() + b"." + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
async def orvenpay_webhook(request: web.Request) -> web.Response:
raw = await request.read()
if not verify(raw, request.headers.get("X-OrvenPay-Signature", "")):
return web.Response(status=400, text="bad signature")
evt = json.loads(raw)
data = evt["data"]
chat_id = (data.get("metadata") or {}).get("chatId")
key = f'{data["id"]}:{evt["event"]}'
if chat_id and key not in processed:
processed.add(key)
bot = request.app["bot"]
if evt["event"] == "invoice.pending":
await bot.send_message(chat_id, "Pago detectado. Esperando confirmaciones de la red...")
elif evt["event"] == "invoice.completed":
await bot.send_message(chat_id, "Pago confirmado. Premium activado.")
elif evt["event"] == "invoice.expired":
await bot.send_message(chat_id, "La factura expiró. Usa /comprar para generar otra.")
return web.Response(text="ok")
async def main() -> None:
tg = Application.builder().token(os.environ["TELEGRAM_BOT_TOKEN"]).build()
tg.add_handler(CommandHandler("comprar", comprar))
app = web.Application()
app["bot"] = tg.bot
app.router.add_post("/orvenpay/webhook", orvenpay_webhook)
runner = web.AppRunner(app)
await runner.setup()
await web.TCPSite(runner, "0.0.0.0", 8080).start() # expón https://tu-bot.com/orvenpay/webhook
async with tg:
await tg.start()
await tg.updater.start_polling()
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())Next.js (App Router)
Cuatro archivos: lib/orvenpay.ts (cliente de la API), app/api/checkout/route.ts (crea la factura), app/api/orvenpay/webhook/route.ts (verifica y procesa webhooks) y .env.local. @/lib/orders representa tu propia capa de pedidos.
// Cliente mínimo de la API (sólo servidor)
const API_URL = process.env.ORVENPAY_API_URL!;
const API_KEY = process.env.ORVENPAY_API_KEY!;
export interface Invoice {
id: string;
status: 'NEW' | 'PENDING' | 'COMPLETED' | 'MISMATCH' | 'EXPIRED' | 'CANCELLED' | 'ERROR';
orderNumber: string;
invoiceUrl: string;
}
export async function createInvoice(order: { id: string; title: string; totalUsd: string }) {
const res = await fetch(`${API_URL}/v1/invoices`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': API_KEY },
body: JSON.stringify({
orderNumber: order.id, // idempotente: reintentar no duplica
orderName: order.title,
sourceCurrency: 'USD',
sourceAmount: order.totalUsd,
}),
cache: 'no-store',
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.invoice as Invoice;
}Next.js
node:crypto). No uses req.json() en el webhook: lee req.text() y parsea después de verificar.Express
// npm i express
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const API = process.env.ORVENPAY_API_URL;
// 1) Webhook ANTES de express.json(): necesita el cuerpo crudo
app.post('/webhooks/orvenpay', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
const p = Object.fromEntries((req.get('X-OrvenPay-Signature') || '').split(',').map((x) => x.trim().split('=')));
const t = Number(p.t);
const expected = crypto.createHmac('sha256', process.env.ORVENPAY_WEBHOOK_SECRET).update(t + '.' + raw).digest('hex');
const ok = /^[0-9a-f]{64}$/.test(p.v1 || '') && Math.abs(Date.now() / 1000 - t) <= 300 &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
if (!ok) return res.status(400).send('bad signature');
const { event, data } = JSON.parse(raw);
if (event === 'invoice.completed') await orders.markPaid(data.orderNumber, data.id); // idempotente
if (event === 'invoice.mismatch') await orders.flagPartial(data.orderNumber, data.amountReceived);
if (event === 'invoice.expired' || event === 'invoice.cancelled') await orders.release(data.orderNumber);
res.sendStatus(200);
});
app.use(express.json());
// 2) Checkout: crea la factura y redirige
app.post('/checkout/:orderId', async (req, res, next) => {
try {
const order = await orders.get(req.params.orderId);
const r = await fetch(API + '/v1/invoices', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': process.env.ORVENPAY_API_KEY },
body: JSON.stringify({
orderNumber: order.id,
orderName: order.title,
sourceCurrency: order.currency, // "USD", "EUR"...
sourceAmount: order.total, // string decimal
successUrl: 'https://mitienda.com/pedido/' + order.id,
failUrl: 'https://mitienda.com/carrito',
}),
});
const body = await r.json();
if (!r.ok) return res.status(502).json(body.error);
res.redirect(303, body.invoice.invoiceUrl);
} catch (e) {
next(e);
}
});
app.listen(3000);PHP / Laravel
<?php
// app/Services/OrvenPay.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class OrvenPay
{
public function createInvoice(string $orderNumber, string $title, string $amountUsd, array $metadata = []): array
{
$res = Http::baseUrl(config('services.orvenpay.url'))
->withHeaders(['X-Api-Key' => config('services.orvenpay.key')])
->timeout(15)
->retry(2, 500, throw: false)
->post('/v1/invoices', [
'orderNumber' => $orderNumber,
'orderName' => $title,
'sourceCurrency' => 'USD',
'sourceAmount' => $amountUsd,
'metadata' => $metadata ?: null,
]);
if ($res->failed()) {
throw new \RuntimeException($res->json('error.code') . ': ' . $res->json('error.message'));
}
return $res->json('invoice');
}
public function validSignature(string $raw, ?string $header): bool
{
$parts = [];
foreach (explode(',', (string) $header) as $pair) {
[$k, $v] = array_pad(explode('=', trim($pair), 2), 2, null);
$parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
if ($t === 0 || abs(time() - $t) > 300 || empty($parts['v1'])) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $raw, config('services.orvenpay.webhook_secret'));
return hash_equals($expected, $parts['v1']);
}
}
// config/services.php
// 'orvenpay' => ['url' => env('ORVENPAY_API_URL'), 'key' => env('ORVENPAY_API_KEY'),
// 'webhook_secret' => env('ORVENPAY_WEBHOOK_SECRET')],Plugins de e-commerce (WooCommerce y similares) vía API Gateway
- Instala un plugin de pasarela cripto que permita configurar la URL de la API y use el formato
api_key+verify_hash(endpoints/api/v1/invoices/new,/api/v1/operations/{id}, callbacks firmados con HMAC-SHA1). - URL base:
https://api.orvenpay.com/gateway(el plugin añadirá/api/v1/...). Clave: una API key de tu tienda. - URL de callback: la que genere el plugin (p. ej.
https://tutienda.com/?wc-api=...); el plugin la envía comocallback_urlen cada factura. - Estados: el plugin marcará el pedido como pagado con
completedymismatch(sobrepago);expiredycancelledlo dejan pendiente/cancelado. - Con plan White Label,
GET /api/v1/shopsdevuelvewhite_label: truey los plugins que lo soportan muestran el pago dentro de la tienda. - Si tu plugin no permite cambiar la URL, usa la API nativa con un pequeño módulo propio (receta PHP de arriba).
18 · Guías
Pruebas
Checklist antes de producción, webhooks en local con un túnel, firmar payloads de prueba y pago real de bajo importe.
No hay entorno sandbox
USDT_BEP20, TRX, LTC o BNB (mínimo 1 USD). Lo cobrado se acredita a tu saldo como cualquier venta.Webhooks en local con un túnel
- Arranca tu servidor local (p. ej.
http://localhost:3000). - Abre un túnel HTTPS público:
cloudflared tunnel --url http://localhost:3000ongrok http 3000. - Usa la URL del túnel como
callbackUrlde la factura de prueba (p. ej.https://abc123.trycloudflare.com/api/orvenpay/webhook).localhosto IPs privadas se rechazan. - Crea una factura pequeña, págala y observa los webhooks
invoice.pendingeinvoice.completed. - En Panel → Facturas → (factura) verás cada entrega con su código HTTP y error, y puedes reenviarla para depurar tu manejador.
Probar tu verificación sin pagar
Firma tú mismo un payload de ejemplo con tu secreto de webhooks y envíalo a tu endpoint. Si tu código acepta esto y rechaza el mismo cuerpo con un byte cambiado, la verificación es correcta:
import crypto from 'node:crypto';
const body = JSON.stringify({
"event": "invoice.completed",
"createdAt": "2026-11-08T14:09:41.000Z",
"data": {
"id": "cmhx3v8qk0004l70gq2b9d7ma",
"storeId": "cmhx2a1bc0001l70g5s6t7u8v",
"orderNumber": "10245",
"orderName": "Plan Pro mensual",
"status": "COMPLETED",
"overpaid": false,
"currency": "USDT_TRC20",
"network": "TRC20",
"amount": "50.91",
"amountReceived": "50.91",
"feeAmount": "0.25455",
"address": "TQ7m4pVb2Xr9KsN3wJd8YfHc6LtUe1gAzR",
"sourceCurrency": "USD",
"sourceAmount": "49.9",
"rate": "0.9998",
"confirmations": 19,
"requiredConfirmations": 19,
"metadata": { "userId": "u_981", "plan": "pro" },
"paidAt": "2026-11-08T14:09:41.000Z",
"expiresAt": "2026-11-08T15:00:00.000Z"
}
});
const t = Math.floor(Date.now() / 1000);
const v1 = crypto.createHmac('sha256', process.env.ORVENPAY_WEBHOOK_SECRET).update(t + '.' + body).digest('hex');
const res = await fetch('http://localhost:3000/api/orvenpay/webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-OrvenPay-Event': 'invoice.completed',
'X-OrvenPay-Delivery': 'test-delivery-1',
'X-OrvenPay-Signature': 't=' + t + ',v1=' + v1,
},
body,
});
console.log(res.status); // esperado: 200Checklist antes de producción
- La API key y el secreto de webhooks están en variables de entorno del servidor, no en el cliente ni en el repositorio.
- Las facturas se crean desde el servidor con un
orderNumberestable por pedido; un reintento tras timeout no duplica facturas. - Manejas
400 AMOUNT_TOO_SMALL(importe mínimo) y503 RATES_UNAVAILABLE(reintento) con mensajes claros. - El webhook verifica
X-OrvenPay-Signaturesobre el cuerpo crudo, con comparación en tiempo constante y ventana de 300 s. - El webhook es idempotente y responde 2xx en menos de 10 s.
- El pedido se entrega sólo con
invoice.completed(no por llegar asuccessUrl). Tienes política parainvoice.mismatch(pago parcial). - Liberas stock/reservas con
invoice.expiredyinvoice.cancelled, y aceptas que unEXPIREDpase aCOMPLETED(pago tardío). - La
callbackUrles HTTPS, pública y sin redirecciones. - Has hecho un pago real de prueba y has visto el crédito en Panel → Saldos (y el retiro a tu libreta, tras la activación de la dirección).
- Activaste 2FA en la cuenta y revisaste quién paga comisión y fee de red en la tienda.
19 · Más
Preguntas frecuentes
Respuestas cortas a las dudas más comunes de integración.
- ¿Puedo crear la factura desde el navegador? No. La API key no debe salir de tu servidor. El navegador sólo recibe
invoiceUrl. - ¿El cliente puede pagar desde un exchange? Sí, siempre que envíe el importe exacto por la red correcta antes de
expiresAt. Algunos exchanges descuentan su comisión del importe: si llega menos de lo esperado (más allá del 0,5 % de tolerancia) la factura acabará enMISMATCH. - ¿Qué pasa si paga de menos? Si lo confirmado alcanza el 99,5 % se completa. Si no, al expirar pasa a
MISMATCHy se acredita lo recibido; decide tú (entregar, pedir la diferencia con otra factura o reembolsar). - ¿Y si paga de más? Se completa con
overpaid: truey se acredita todo lo recibido hasta completar. Si quieres devolver el exceso, hazlo con un retiro. - ¿Y si paga tarde? Hasta 24 h después de expirar, el pago se procesa y recibes
invoice.completed(oinvoice.mismatch). - ¿Puedo cambiar el importe de una factura? No. Cancela la factura (si está en
NEW) y crea otra con otroorderNumber. - ¿Se puede reutilizar una dirección? No. Cada factura tiene su propia dirección; un pago a la dirección de una factura antigua cerrada queda para revisión de soporte.
- ¿En qué moneda recibo el dinero? En la misma cripto que pagó el cliente (sin conversión). Retiras cada moneda por separado.
- ¿Cuánto tarda en confirmarse? Depende de la red: TRON ~1 min (19 confirmaciones), BNB Chain ~45 s (15), Ethereum ~3 min (12), Litecoin ~15 min (6), Bitcoin ~30 min (3).
- ¿Cuándo puedo retirar lo cobrado? 1 h después de acreditarse, a direcciones de tu libreta activas (24 h tras añadirlas).
- ¿Necesito White Label para usar mi propio checkout? Sí: sólo las tiendas White Label reciben la dirección e importe por API. Las estándar usan la página de pago alojada.
- ¿Puedo usar un plugin existente de pasarela cripto? Sí, si permite cambiar la URL de la API: usa la API Gateway con base
https://api.orvenpay.com/gateway. - ¿Por qué no me llegan webhooks? Revisa que la
callbackUrles pública y HTTPS, que no redirige, que responde 2xx en < 10 s y que verificas la firma sobre el cuerpo crudo. El panel muestra el último código HTTP y error de cada entrega. - ¿Hay SDK oficial? No hace falta: es REST + JSON. Usa la especificación OpenAPI (https://orvenpay.com/openapi.json) para generar un cliente, o las recetas.
- ¿Dónde está todo esto para mi agente de IA? En https://orvenpay.com/llms-full.txt (un único Markdown), https://orvenpay.com/llms.txt (índice) y https://orvenpay.com/openapi.json.
- ¿Puedo usar el logotipo para anunciar que aceptáis pagos con OrvenPay? Sí, sin modificarlo: logotipo para fondos oscuros, para fondos claros, monocromo blanco y símbolo en SVG.
20 · Más
Changelog
Cambios relevantes de la API y la documentación.
¿Listo para empezar?
Crea tu cuenta, activa tus monedas y emite tu primera factura de prueba.
Última revisión: 2026-11-08.