Saltar al contenido principal

Quickstart

De cero a un pago en cinco llamadas. Necesitas tu clientId, tu clientSecret y la clave privada RSA de tu aprovisionamiento — si te falta alguna, consulta el overview.

Todos los ejemplos de abajo corren contra sandbox:

https://api.gateway.sandbox.retorna.app

Cuando salgas a producción lo único que cambia es el host — y tus credenciales, que se emiten por ambiente. Production es https://api.gateway.retorna.app y su scope es prod/full_access.

1. Conseguir un token​

curl -X POST https://api.gateway.sandbox.retorna.app/oauth2/token \
-H "Authorization: Basic $(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "scope=sandbox/full_access"

Respuesta:

{ "access_token": "eyJraWQiOiJ...", "expires_in": 3600, "token_type": "Bearer" }

Guárdalo en caché y reutilízalo hasta que esté cerca de expirar. No hay refresh token: pides otro y listo.

2. Firmar lo que envías​

Este es el paso que todos se saltan, y después todo devuelve 401. Cada llamada de aquí en adelante necesita dos headers extra:

import { createSign } from "node:crypto";

const nonce = String(Date.now()); // epoch MILLISECONDS
const message = mode === "query"
? `${path}?${queryString}${nonce}` // GETs
: JSON.stringify(rawBody) + nonce; // POSTs — note the second encode

const signer = createSign("RSA-SHA256");
signer.update(message);
signer.end();
const signature = signer.sign(privateKeyPem, "base64");

EnvĂ­a signature y nonce junto con tu header Authorization. La guĂ­a de firma de solicitudes tiene las reglas completas, la versiĂłn en Python y los dos errores que le cuestan una tarde a cualquiera.

Cada llamada de acá en adelante también necesita User-Agent y Accept — mirá headers requeridos si estás en un cliente que no los manda por defecto.

3. Ver tus rutas​

¿Qué puedes enviar hoy y cuánto margen queda en tus límites?

curl "https://api.gateway.sandbox.retorna.app/me/routes?sourceCurrency=USDR" \
-H "Authorization: Bearer $TOKEN" \
-H "signature: $SIGNATURE" -H "nonce: $NONCE"

Respuesta:

{
"routes": [
{
"name": "P2P_PHONE_TRANSFER-VE",
"method": "P2P_PHONE_TRANSFER",
"country": "VE",
"currency": "VES",
"rate": "852.79",
"fees": [ { "pricingRule": "PAYOUT_TYPE_FEE", "pricingType": "FEE", "percentageValue": 1, "flatValue": 0.25, "pricingLabel": "ComisiĂłn" } ],
"limits": { "per_transaction": { "min": "0", "max": "99000" } }
}
]
}

Consulta los lĂ­mites en runtime en vez de hardcodearlos: se mueven con tu consumo.

4. Fijar un precio​

POST /quotations, firmado en modo body:

curl -X POST https://api.gateway.sandbox.retorna.app/quotations \
-H "Authorization: Bearer $TOKEN" \
-H "signature: $SIGNATURE" -H "nonce: $NONCE" \
-H "Content-Type: application/json" \
-d @quotation.json

quotation.json:

{
"source": { "currency": "USDR" },
"destination": {
"country": "VE",
"currency": "VES",
"payout_method": { "type": "P2P_PHONE_TRANSFER" }
},
"quote": { "mode": "SEND_EXACT", "amount": "10.00" }
}

Respuesta:

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"source": { "currency": "USDR", "decimals": 2, "gross_amount": "10.00", "net_amount": "9.90" },
"destination": { "country": "VE", "payout_method": { "type": "P2P_PHONE_TRANSFER" } },
"target": { "currency": "VES", "decimals": 2, "amount": "8442.62" },
"exchange_rate": { "value": "852.79", "quoted_at": "...", "expires_at": "..." },
"fees": { "total_in_source_currency": "0.10", "breakdown": [ ] }
}

Revisa el exchange_rate.expires_at: pasado ese momento la quotation deja de ser válida y necesitas una nueva. Y guarda destination.payout_method.type junto a la orden que vas a crear: las instrucciones de pago tienen que coincidir con él (bank_phone_account para P2P_PHONE_TRANSFER, bank_account para BANK_TRANSFER), o la orden se rechaza con 422 B2B_QUOTATION_MISMATCH.

5. Enviar el pago​

POST /orders, firmado en modo body, con el id de la quotation y el beneficiario. El header X-Idempotency-Key es obligatorio — elige algo único y con sentido en tu propio sistema, porque vuelve como idempotency_key y es cómo vas a encontrar esta orden después.

curl -X POST https://api.gateway.sandbox.retorna.app/orders \
-H "Authorization: Bearer $TOKEN" \
-H "signature: $SIGNATURE" -H "nonce: $NONCE" \
-H "X-Idempotency-Key: invoice-2026-00456" \
-H "Content-Type: application/json" \
-d @order.json

order.json:

{
"quotation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"metadata": { "invoice_id": "INV-2026-00456", "batch_id": "BATCH-VE-032" },
"purpose": "FAMILY_SUPPORT",
"sender": {
"names": "Ana MarĂ­a",
"last_names": "Pérez Gómez",
"document": { "type": "CC", "id": "1020304050", "country": "CO" },
"country": "CO"
},
"destination": {
"receiver": {
"names": "Carlos Andrés",
"last_names": "RodrĂ­guez LĂłpez",
"document": { "type": "V", "id": "12345678", "country": "VE" },
"email": "[email protected]",
"country": "VE"
},
"payment_instructions": {
"bank_phone_account": {
"bank_name": "BNC_VE",
"phone_number": "+584121234567",
"document_id": "12345678"
}
}
}
}

bank_name es el nombre API del catálogo, no el nombre comercial del banco — BNC_VE, nunca "BNC". La lista completa está en entidades bancarias.

Respuesta:

{
"id": "cpg_txn_a1b2c3d4e5f6",
"idempotency_key": "invoice-2026-00456",
"status": "ACCEPTED",
"quotation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"metadata": { "invoice_id": "INV-2026-00456", "batch_id": "BATCH-VE-032" },
"created_at": "2026-09-10T14:31:00.000Z",
"updated_at": "2026-09-10T14:31:00.000Z"
}

Abreviada: la orden completa también devuelve sender y destination y agrega execution y events. Mira crear orden.

6. Seguirla​

Dos formas, y en producciĂłn quieres la segunda:

curl "https://api.gateway.sandbox.retorna.app/orders/{id}" \
-H "Authorization: Bearer $TOKEN" \
-H "signature: $SIGNATURE" -H "nonce: $NONCE"

Respuesta:

{
"id": "cpg_txn_a1b2c3d4e5f6",
"idempotency_key": "invoice-2026-00456",
"status": "PENDING",
"quotation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"metadata": { "invoice_id": "INV-2026-00456", "batch_id": "BATCH-VE-032" },
"created_at": "2026-09-10T14:31:00.000Z",
"updated_at": "2026-09-10T14:31:04.000Z"
}

O nos dejas avisarte: cada cambio de estado se entrega a tu endpoint como webhook. Deduplica por eventId, confĂ­a en el timestamp antes que en el orden de llegada, y verifica la firma antes de confiar en el body.

Si te devuelve 401​

En orden de frecuencia:

  1. El nonce está en segundos, no en milisegundos.
  2. Re-serializaste el body JSON después de firmarlo — firma los bytes exactos que envías.
  3. Te saltaste la segunda codificaciĂłn en modo body.
  4. El token expirĂł.

Un 401 con campo code es tu firma; un 401 con campo error es tu token. Ese solo campo te ahorra mucha adivinanza.