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" },
"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:
- El
nonceestá en segundos, no en milisegundos. - Re-serializaste el body JSON despuĂ©s de firmarlo — firma los bytes exactos que envĂas.
- Te saltaste la segunda codificaciĂłn en modo body.
- 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.