Saltar al contenido principal

Conducir el ciclo en el simulador

En producción no puedes decidir que un pago salga mal: el pago es real. En el simulador de SPIDI sí, y esa es la razón de que exista. Aquí conduces la sesión al desenlace que quieras, tantas veces como quieras, y compruebas qué hace tu código con cada uno.

Necesitas tus dos tokens: Credenciales.

Esto es el simulador, no producción

POST /control/sessions/<id>/outcome, POST /control/reset y las claves de pago mágicas de la página de pago (000000 y compañía) existen solo en el simulador. En producción no hay forma de forzar un desenlace: lo decide el banco. Si tu código de integración llama a /control/**, falla el día que salgas del sandbox — úsalo desde tus pruebas, nunca desde el camino que va a producción.

1. El acuerdo

El acuerdo define cómo recibes el dinero. Se crea una vez y lo reutilizas en todas las sesiones. El concepto está en Acuerdo de liquidación.

curl -X POST https://sim-productos.abiertolab.com/api/v1/ext/agreements \
-H "Authorization: Bearer <tu-token-de-api>" \
-H "Content-Type: application/json" \
-d '{
"title": "Acuerdo de pruebas",
"description": "Sin distribución de fondos",
"split": false,
"payment_methods": { "immediate_debit": true, "crypto": false, "mobile_payment": true },
"default_bank_account_id": "uuid_banco_001"
}'

La respuesta trae data.agreement_id (agr_…). Guárdalo.

2. La sesión

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

La respuesta trae data.session_id (ses_…) y data.payment_url. La sesión nace en pending.

Los ocho campos de arriba son obligatorios: agreement_id, amount_reference, currency_reference, identifier_label, identifier, description, success_url, failure_url. Si falta alguno recibes un 400. El webhook_url es opcional, pero sin él no recibes ningún aviso.

El simulador no convierte la moneda de referencia

Ese ejemplo pide 50 USD y el crédito que vas a ver es de unos 50 bolívares, no los varios miles que darían al cambio. El simulador valida la moneda —acepta USD, EUR, COP, USDT y VES, y rechaza cualquier otra con un 400— pero no aplica ninguna tasa: trata el amount_reference como si ya viniera en bolívares.

Es a propósito, y es la misma frontera de siempre: el simulador es autoridad sobre la forma, no sobre los números. La tasa la fija el BCV y se mueve a diario; congelarla aquí sería inventar una cifra que a los dos días miente.

Qué significa para tus pruebas: puedes comprobar que tu código lee bien amount_ves_credited, que resta la comisión y que concilia — pero no uses el sandbox para verificar tu aritmética de conversión. Para eso, fija tú la tasa en tus propias pruebas unitarias.

3. Forzar el desenlace

El plano de control lleva la sesión adonde tú digas. Va con el token de API (el mismo con el que creaste la sesión) y solo toca tus datos.

curl -X POST https://sim-productos.abiertolab.com/control/sessions/<session_id>/outcome \
-H "Authorization: Bearer <tu-token-de-api>" \
-H "Content-Type: application/json" \
-d '{"leg":1,"outcome":"paid"}'

Responde { "success": true, "data": { … } } con la sesión ya actualizada.

El pago tiene dos fases: el débito (fase 1) y la acreditación al receptor (fase 2). Por qué son dos y qué implica está en Transacción a dos fases y en Ciclo de vida de la sesión. Esto es lo que acepta cada uno:

legoutcomeSe admite desdeQué pasaWebhook que emite
1paidpendingEl status pasa a paidpayment_session.paid
1failedpending, y solo en BotónEl status pasa a failedninguno
1expiredpendingEl status pasa a expiredninguno
2accreditedpaidLa sesión queda acreditada; el status sigue paidpayment_session.accredited
2accreditation_failedpaidEl status sigue paid; no queda rastro en la sesiónpayment_session.accreditation_to_recipient_failed

Tres cosas que conviene saber antes de pelearte con ellas:

  • failed no aplica a la Solicitud de pago. Una Solicitud o se paga o vence. Si lo intentas, la llamada falla.
  • Una transición inválida responde 500, con el motivo en el campo message (por ejemplo leg1 inválido desde estado paid, o leg2 requiere status paid). Es feo, pero te dice exactamente qué pasó.
  • Forzar paid por el plano de control no programa la acreditación. La fase 2 queda esperando solo cuando el pago entra por la página de pago. Si fuerzas paid con /control y esperas sentado el payment_session.accredited, no llega nunca: fuerza también la fase 2.

Para dejar tu cuenta como recién creada entre pruebas:

curl -X POST https://sim-productos.abiertolab.com/control/reset \
-H "Authorization: Bearer <tu-token-de-api>"

Borra solo tus sesiones, acuerdos y eventos.

4. Pagar como paga una persona

En vez de forzar, puedes abrir la payment_url de la sesión en un navegador. Es la página de pago del simulador: te deja elegir método de pago entre los que tu acuerdo declaró en payment_methods —pago móvil, débito inmediato o cripto, y solo esos—, pide los datos que ese método necesita, arranca un temporizador de 30 segundos y pide una clave de pago. El método que uses viaja después en payment_method, así que puedes cruzar lo que declaraste con lo que te llegó. Al confirmar redirige a tu success_url o a tu failure_url, igual que en una integración real.

Las claves de pago son mágicas, y cada una provoca un desenlace distinto:

ClaveResultadoMotivo que queda en session_payment
000000paid
111111failedFondos insuficientes
222222failedClave de pago incorrecta
333333failedLímite diario del banco excedido
dejar vencer el temporizadorexpiredLa sesión de pago expiró

Cualquier otra clave se toma como incorrecta.

Antes de confirmar, la página te deja elegir cómo termina la segunda fase: acreditación exitosa o acreditación fallida. Confirmas, el pago queda paid al instante, y unos segundos después el simulador aplica solo el desenlace que elegiste y te entrega el payment_session.accredited o el payment_session.accreditation_to_recipient_failed correspondiente — sin que llames a /control/**. Esa espera es deliberada: pagado no es recibido, y aquí lo ves llegar aparte.

Si después de eso fuerzas la fase 2 por el plano de control, tu orden explícita gana: el simulador cancela la acreditación que tenía programada, para no entregarte dos avisos contradictorios del mismo pago.

5. Recibir el webhook en tu servidor

Aquí está la parte que más gente se salta y más caro sale.

El simulador trae receptores por defecto en /console/hooks/<account_id>/paid, /accredited y /fail (los ves en GET /console/keys). Los dos primeros responden 200 siempre; el tercero responde 500 a propósito, para que veas los reintentos y la rendición en la traza.

Hay un cuarto, /captura, que acepta cualquier cosa y responde 200. Todo lo que llega a cualquiera de los cuatro queda en la traza como webhook.received, con el cuerpo crudo, las cabeceras y la firma — útil cuando quieres ver qué forma tiene un aviso antes de escribir el código que lo procesa.

Son cómodos para ver la mecánica, y no prueban absolutamente nada de tu código: son del propio simulador, así que estás contestándote a ti mismo. Tu verificación de firma no se ejecuta, tu deduplicación no se ejecuta, tu manejo de errores no se ejecuta. En la traza esos envíos aparecen con routing: "loopback", que es la señal de que no estás probando lo que crees.

Para que el aviso llegue a tu código hay dos vías.

Vía A — una URL pública

Pon en webhook_url una URL alcanzable desde internet: tu servicio ya desplegado, o un túnel (por ejemplo ngrok) apuntando a tu puerto local. En la traza esos envíos aparecen con routing: "direct".

Vía B — el relay, si tu receptor vive en localhost

El simulador hosteado no puede alcanzar tu localhost, pero tu máquina sí puede alcanzarlo a él. El relay usa eso: abre un WebSocket saliente contra el simulador, se queda escuchando, y cuando baja un webhook lo entrega con un POST a tu servidor local.

Creas la sesión con tu URL local tal cual:

{ "webhook_url": "http://localhost:4020/webhooks/spidi" }

Y dejas el relay corriendo en otra terminal:

npx https://docs.abiertolab.com/relay/spidi-sim-relay.tgz --key <tu-token-de-api> --mock wss://sim-productos.example
  • --key es el token de API (no el de consola): así el simulador sabe a qué cuenta pertenece ese relay.
  • --mock es la URL del simulador con esquema WebSocket. Con el simulador hosteado —que es tu caso— es el mismo host de https://sim-productos.abiertolab.com con esquema wss://. El ws://localhost:… solo aplica si levantas el simulador en tu máquina, que es cosa de desarrollo del propio simulador, no de integrar.

El simulador clasifica automáticamente como relay cualquier webhook_url que apunte a localhost, 127.0.0.1 o a una red privada (10.x, 192.168.x, 172.16-31.x). No hay nada más que configurar. En la traza esos envíos aparecen con routing: "relay".

Solo puede haber un relay conectado por key: si abres un segundo, el primero se cierra.

Qué hace tu receptor

Verificar la firma —que se calcula sobre spidi-timestamp + "." + el cuerpo crudo—, deduplicar por idempotency-key y responder 2xx rápido. Está desarrollado en Manejar notificaciones. Si algo de eso no sale, el catálogo de síntomas está en Observar y depurar.

Un ciclo completo, de arriba abajo

  1. POST /console/register → tus tokens.
  2. POST /api/v1/ext/agreementsagreement_id.
  3. Arranca tu receptor y, si es local, el relay.
  4. POST /api/v1/ext/payment-sessions/buttons con tu webhook_urlsession_id.
  5. POST /control/sessions/<session_id>/outcome con {"leg":1,"outcome":"paid"} → te llega payment_session.paid.
  6. POST /control/sessions/<session_id>/outcome con {"leg":2,"outcome":"accredited"} → te llega payment_session.accredited.
  7. GET /api/v1/ext/payment-sessions/status/<session_id>status: "paid".
  8. Repite el paso 5 con failed y con expired en sesiones nuevas, y el paso 6 con accreditation_failed. Los cinco desenlaces cubiertos.

Observar y depurar · Ciclo de vida de la sesión