Saltar al contenido principal

Recibe tu primer pago

¡Hola! En unos minutos vas a recibir tu primer pago de prueba con SPIDI, de punta a punta: lo creas, lo llevas a pagado y confirmas que te llegó el aviso. Que el dinero fluya. 🐦

Corre contra el simulador

Este tutorial usa el simulador hosteado de SPIDI: ábrelo en otra pestaña y lo tienes al lado mientras sigues los pasos. Te registras y completas un pago de prueba sin dinero real y sin credenciales en vivo. → Usar el simulador.

Verificado automáticamente

Los comandos de los pasos 1 a 5 se ejecutan contra el simulador en cada cambio (prueba simulador/test/func/tutorial.func.test.ts). Si alguno dejara de funcionar, el CI lo detecta. El Paso 0 todavía no está cubierto por esa prueba — se dice para no prometer de más.

Recibe tu primer pago con SPIDIGuion: .docx · .yaml

Antes de empezar

  • El simulador hosteadohttps://sim-productos.abiertolab.com. No hay que instalar ni desplegar nada tuyo; te registras en el Paso 0.
  • curl para los ejemplos y un poco de Node para el receptor del aviso.
  • No necesitas credenciales reales: el registro te entrega una cuenta de prueba.

Paso 0 — Regístrate

Objetivo: obtener tus credenciales.

curl -X POST https://sim-productos.abiertolab.com/console/register \
-H "Content-Type: application/json" \
-d '{"email":"tu@correo.test","password":"lo-que-quieras"}'

Respuesta, y conviene saber para qué sirve cada pieza porque no son intercambiables:

{
"console_token": "...",
"key": {
"account_id": "...",
"token": "...",
"webhook_secret": "whsec_...",
"short_name": "...",
"password": "apw_..."
}
}
PiezaPara qué sirve
key.tokenEl Bearer del API. Es el que usan todos los pasos de este tutorial
console_tokenLa consola y la traza (GET /console/trace), y tus receptores de webhook
key.short_name + key.passwordLas credenciales de POST /api/spidipagos/login, para que puedas ejercitar el camino de autenticación que en producción es obligatorio

Cruzarlos devuelve 401, y ese 401 significa «token equivocado», no «token inválido».

En producción no haces este paso: obtienes el token con tu login real (/api/spidipagos/login), con las credenciales que te entregue SPIDI.

Detalle completo de las tres piezas, sus errores y la rotación del token de consola: → Credenciales del simulador.

Paso 1 — Crea tu acuerdo de liquidación

Objetivo: definir cómo recibes el dinero (agreement_id).

curl -X POST https://sim-productos.abiertolab.com/api/v1/ext/agreements \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"title": "Acuerdo sin Split",
"description": "Sin distribución de fondos",
"split": false,
"payment_methods": { "immediate_debit": true, "crypto": false, "mobile_payment": true },
"default_bank_account_id": "uuid_sofitasa_001",
"rules": [{ "origin_bank_code": "0105", "destination_bank_account_id": "uuid_mercantil_007" }]
}'

Qué observar: la respuesta trae data.agreement_id —cuelga de data, no de la raíz—. Checkpoint: tienes un agreement_id.

Paso 2 — Crea la sesión de Botón

Objetivo: generar el enlace de pago (payment_url).

curl -X POST https://sim-productos.abiertolab.com/api/v1/ext/payment-sessions/buttons \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"amount_reference": 50,
"currency_reference": "USD",
"identifier_label": "Pedido",
"identifier": "ORD-0001",
"description": "Mi primer pago de prueba",
"success_url": "https://miapp.com/pago-exitoso",
"failure_url": "https://miapp.com/pago-fallido",
"agreement_id": "<agreement_id>",
"webhook_url": "https://tu-app.example/webhooks/spidi"
}'

Para que el simulador hosteado te entregue el aviso tienes dos vías. Una es un webhook_url accesible desde internet: tu servicio desplegado, o un túnel tipo ngrok apuntando a tu puerto local. La otra es el relay, si tu receptor vive en localhost (p. ej. http://localhost:4020): abre un WebSocket saliente contra el simulador y le entrega el aviso a tu puerto, sin que tengas que exponer nada. → Conducir el ciclo.

Qué observar: data.payment_url y data.session_id. Checkpoint: tienes la payment_url y el session_id (ambos dentro de data). La sesión nace en pending; lo confirmas en el Paso 4 con GET status.

Los campos identifier_label, identifier y description son obligatorios (además de montos y URLs) — el contrato los exige.

Paso 3 — Lleva a quien paga al payment_url

Objetivo: que la persona complete el pago en la página de SPIDI.

<a href="<payment_url>" class="btn-spidi">Pagar con SPIDI</a>

En el simulador puedes abrir la payment_url en el navegador para ver la página de pago real (formulario de Pago Móvil + clave OTP), o forzar el desenlace desde el plano de control (siguiente paso). En la página, las claves OTP de prueba te dejan provocar cada resultado a mano — 000000 paga; otras fallan con su motivo. → Usar el simulador.

Paso 4 — Confirma el pago del lado del servidor

Lección clave

La redirección a tu success_url no garantiza que te pagaron — solo dice que la persona volvió. Pregúntale a SPIDI desde tu backend.

# 1) en el simulador, forzamos el desenlace (sin pago real):
curl -X POST https://sim-productos.abiertolab.com/control/sessions/<session_id>/outcome \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"leg":1,"outcome":"paid"}'

# 2) consultamos el estado REAL:
curl https://sim-productos.abiertolab.com/api/v1/ext/payment-sessions/status/<session_id> \
-H "Authorization: Bearer <token>"

Qué observar: status: paid. Checkpoint: tu backend confirmó paid. Si sigue pending: nunca des por bueno el pago por la sola redirección.

Paso 5 — Recibe y valida el aviso de pago (webhook)

Al llegar a paid, el simulador envía a tu webhook_url el evento payment_session.paid con los headers spidi-signature (HMAC-SHA256 de spidi-timestamp + "." + el cuerpo crudo), spidi-timestamp e idempotency-key.

// Node — verificar firma (timestamp + "." + cuerpo CRUDO) + idempotencia
import { createHmac, timingSafeEqual } from "crypto";

app.post("/webhooks/spidi", (req, res) => {
const ts = req.headers["spidi-timestamp"];
const recibida = req.headers["spidi-signature"];
if (!ts || !recibida) return res.status(401).end(); // sin timestamp no hay qué verificar

const firma = createHmac("sha256", WEBHOOK_SECRET).update(`${ts}.${req.rawBody}`).digest("hex");
const ok = firma.length === recibida.length &&
timingSafeEqual(Buffer.from(firma), Buffer.from(recibida));
if (!ok) return res.status(401).end(); // firma inválida
if (yaVisto(req.headers["idempotency-key"])) return res.status(200).end(); // repetido
procesar(req.body); marcarVisto(req.headers["idempotency-key"]);
res.status(200).end();
});

Qué observar: la firma coincide; un aviso repetido (mismo idempotency-key) no se procesa dos veces. Si falla: firma inválida → 401; repetido → 200 sin reprocesar.

Dos cosas que hay que respetar o la verificación falla: la cadena firmada es spidi-timestamp + "." + cuerpo, y el cuerpo son los bytes recibidos, no el JSON re-serializado — un byte de diferencia invalida la firma. → Manejar notificaciones.

Paso 6 — ¡Listo! Y cómo te llega el dinero

Acabas de recibir tu primer pago con SPIDI: lo creaste, lo confirmaste del lado del servidor y procesaste el aviso de forma segura.

El flujo del dinero: el pago se debita (fase 1) y luego se acredita al receptor (fase 2, ya descontada la comisión del banco) — y eso viaja por un segundo webhook, payment_session.accredited. Ojo: el status nunca pasa de paid por la acreditación. La fase 2 la ves en el webhook, o en data.session_payment.receiver_credits. → Transacción a dos fases.

Próximos pasos: Solicitud de pago · Split · pasar a producción.