Saltar al contenido principal

El simulador como API

El simulador es un sandbox hosteado que emula el contrato de la API Productos de SPIDI: la máquina de estados de la sesión, la acreditación a dos fases y los webhooks firmados. Te registras, obtienes tus credenciales y trabajas en tu propio espacio aislado — nadie más ve ni afecta tus datos de prueba. Todo sin tocar producción ni mover dinero real.

Esta página es la referencia de la herramienta: qué superficie expone, qué hace con cada llamada y qué garantías da. Todo lo de aquí se maneja por HTTP, sin navegador. Si lo que buscas es el camino paso a paso para probar tu integración, ese es otro: Probar tu integración.

¿Por qué un simulador?

El entorno de pruebas en vivo no deja forzar un paid o un failed (el pago es un formulario real) ni dispara webhooks de prueba. El simulador sí: provocas el desenlace que quieras y recibes el aviso firmado, de forma reproducible.

El simulador como API: contrato, plano de control y webhooksGuion: .docx · .yaml

Está en pie y responde ahora mismo: https://sim-productos.abiertolab.com. No hay que instalarlo ni desplegarlo — se abre y se usa, y su consola vive en esa misma dirección.

La distinción que más caro sale ignorar

El simulador expone dos superficies mezcladas en el mismo host, y solo una de las dos existe en producción:

Superficie¿Existe en producción?Para qué
POST /api/spidipagos/loginAutenticarte. Es contrato
/api/v1/ext/** (17 operaciones)Acuerdos, partners, sesiones, paradas, status. Es contrato
/console/**NoAlta self-service, traza de eventos, retos, limpieza
/control/**NoForzar desenlaces y reiniciar tu cuenta
/pay/:idNoLa página de pago espejo, con claves de prueba
POST /registerNoAlta histórica, sin token de consola. Se mantiene por compatibilidad; no la uses
GET /healthNoSonda de vida del propio simulador
Andamiaje dentro del camino a producción = rotura garantizada

Todo lo marcado No desaparece el día que apuntes a producción. Úsalo desde tus pruebas, nunca desde el código que va a desplegarse. Un POST /control/... colado en la ruta de integración no falla hoy: falla el día del estreno.

Credenciales: dos tokens que no se cruzan

El alta vigente es POST /console/register, y entrega de una vez las tres piezas: el token de API con el que integras, el token de consola con el que lees la traza, y el webhook_secret con el que verificas las firmas.

PiezaAbreNo abre
key.token (token de API)/api/v1/ext/** y /control/**/console/**
console_token/console/**la API y el plano de control
key.webhook_secret(no viaja en ninguna petición)

Cruzarlos devuelve 401, y ese 401 significa "token equivocado", no "token inválido". El detalle completo —respuestas, errores, la rotación del token de consola al hacer login, y el alta histórica POST /register— está en Credenciales del simulador.

El plano de control

Dos operaciones. Van con el token de API y solo tocan tus datos.

# llevar una sesión al desenlace que quieras
POST /control/sessions/<session_id>/outcome {"leg": 1|2, "outcome": "..."}

# dejar tu cuenta como recién creada (borra solo lo tuyo)
POST /control/reset

Los cinco desenlaces posibles, con qué webhook emite cada uno y desde qué estado se admiten, están tabulados en Conducir el ciclo. Tres comportamientos del simulador que conviene saber porque no se deducen:

  • Una transición inválida responde 500, con el motivo en message (leg1 inválido desde estado paid, leg2 requiere status paid). Es feo y es informativo.
  • Forzar paid por el plano de control NO programa la acreditación. La fase 2 solo queda programada cuando el pago entra por la página de pago. Si fuerzas paid y esperas el payment_session.accredited, no llega: fuerza también la fase 2.
  • Tu orden explícita gana. Si fuerzas la fase 2 sobre una sesión que ya tenía acreditación programada, el simulador cancela la programada para no entregarte dos avisos contradictorios del mismo pago.

Cada ruta /control/** tiene un alias /__test__/** idéntico, herencia del arranque del proyecto. Usa /control/**.

El refresco de token

POST /api/spidipagos/refresh-token {"refreshToken": "…"}

Devuelve un token y un refreshToken nuevos, igual que el login. Existe aquí porque existe en producción, aunque el contrato no lo declare: si el simulador respondiera 404, quien siguiera la guía de autenticación se daría contra un muro justo al hacerlo bien.

Dos diferencias que conviene saber:

  • El token de API del simulador no caduca, así que aquí refrescar es ejercitar la mecánica, no una necesidad.
  • El refresh anterior sigue siendo válido después de refrescar. No te apoyes en lo contrario: fuera del simulador no está garantizado.

Cuánto tarda la acreditación

Unos 5 segundos tras el pago, configurable con SIM_ACCREDITATION_DELAY_MS.

Ese número es didáctico, no realista

Está puesto corto para que veas las dos fases en una sola pantalla. En un entorno real la acreditación tarda entre uno y dos minutos, y el aviso llega unos segundos después de que el dinero ya está.

No escribas lógica que dependa de que la acreditación llegue rápido, ni pruebas que fallen si tarda. → Transacción a dos fases

Cómo emite los webhooks

Esto es lo que hace el simulador cuando le toca avisarte. Lo que tiene que hacer tu receptor está en Manejar notificaciones.

Los créditos de la acreditación

Tras acreditar, GET status incluye receiver_credits y receiver_credits_summary dentro de session_payment, y el webhook los trae al mismo nivel que session_payment. Esa diferencia de ubicación es la del contrato, y el simulador la reproduce: si tu parser solo funciona con una de las dos fuentes, aquí lo descubres.

Con split, cada receptor aparece con su crédito y la comisión del banco se aplica por crédito, no sobre el total de la sesión — igual que en producción. El total_credits del resumen cuenta los receptores del reparto más el owner, que es la comprobación que conviene hacer al conciliar.

Los cuatro eventos

payment_session.created · payment_session.paid · payment_session.accredited · payment_session.accreditation_to_recipient_failed

Solo el de acreditación trae receiver_credits_summary con el neto y las comisiones; los demás llevan session_payment con id, origin y amount_reference.

Las cabeceras de cada envío

CabeceraContenido
content-typeapplication/json
spidi-signatureHMAC-SHA256 de spidi-timestamp + "." + el cuerpo crudo, en hexadecimal, con tu webhook_secret
spidi-timestampMarca de tiempo de la sesión, en ISO 8601. Entra en la firma
idempotency-keyUn identificador por emisión

La cadena que se firma es spidi-timestamp, un punto, y el cuerpo crudo byte a byte. Dos formas de equivocarse, y las dos dan el mismo síntoma —la firma no coincide—: dejarse el timestamp fuera del cálculo, o firmar el JSON re-serializado en vez de los bytes que llegaron. Guarda el cuerpo sin tocar y usa el header tal cual.

Reintentos y rendición

Un intento por cada espera de la escalera —0 · 2000 · 5000 ms, en ese orden—, con 5000 ms de timeout cada uno. Un 2xx cierra la entrega; agotados los intentos, el simulador se rinde y lo registra como tal en la traza.

Este calendario es del simulador, no de producción

Las fases de arriba son los del simulador y están puestos para que veas la secuencia completa en segundos, no para reproducir el ritmo de producción. Lo que sí puedes dar por bueno es la forma —hay reintentos, hay límite y hay rendición—, no los números. No escribas lógica que dependa de que el segundo intento llegue a los 2 s.

La idempotency-key no cambia entre reintentos de una misma emisión. Ese es justamente el punto: si tu receptor tardó y respondió tarde, vas a recibir el mismo aviso otra vez con la misma clave, y deduplicar por ella es lo que evita que le cargues el pago dos veces a tu cliente.

Captura primero

El simulador registra el webhook en la traza aunque no tenga a dónde entregarlo — sesión sin webhook_url, receptor caído, relay desconectado. El aviso queda con su payload y su firma. Es deliberado: un webhook que nunca salió es exactamente la información que necesitas cuando no entiendes por qué tu servidor no recibe nada.

Cómo decide a dónde entrega: loopback, direct, relay

El simulador clasifica cada webhook_url automáticamente. No hay nada que configurar, y la clasificación aparece en la traza como routing.

routingCuándoQué significa para ti
loopbackLa URL apunta al propio simulador, a /console/hooks/<account_id>/(paid|accredited|fail)No estás probando nada tuyo. Te estás contestando a ti mismo
relayEl host es local o privado: localhost, 127.0.0.1, 0.0.0.0, ::1, 10.x, 192.168.x, 172.16-31.x — o la URL no se puede parsearLa entrega va por el relay, si lo tienes conectado
directCualquier otro hostEl simulador hace el POST a tu servidor por internet
loopback es la trampa que hay que conocer

Los receptores por defecto (/console/hooks/…, los que ves en GET /console/keys) responden siempre: dos con 200 y el de fail con 500 a propósito. Sirven para ver la mecánica de entrega funcionando sin montar nada, y no ejecutan una sola línea de tu código: ni tu verificación de firma, ni tu deduplicación, ni tu manejo de errores. Si todo te sale verde y el routing dice loopback, no has probado tu integración.

Todo lo que entra por un receptor queda en la traza

Cada POST que llega a /console/hooks/… se registra como webhook.received, con:

El cuerpo crudoTal cual llegó. Con él y el spidi-timestamp de las cabeceras se reconstruye la cadena firmada; reserializar el JSON la invalida
El cuerpo parseadoPara leerlo cómodo
Las cabecerasIncluidas spidi-signature e idempotency-key, ya extraídas aparte

Y hay un cuarto receptor, captura, que acepta cualquier cosa:

POST /console/hooks/<account_id>/captura

Responde 200 a todo y lo deja en tu traza. Los otros tres modelan desenlaces de entrega; este es un microscopio: apúntale lo que sea —incluso avisos de otro sistema— y mira exactamente qué te habría llegado, byte a byte.

Filtra la traza por webhook.received para verlos.

El relay

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

npx https://docs.abiertolab.com/relay/spidi-sim-relay.tgz --key <tu-token-de-api> --mock https://sim-productos.abiertolab.com
  • --key es el token de API, no el de consola: es como el simulador sabe a qué cuenta pertenece ese relay.
  • --mock es la URL del simulador. El relay la convierte al esquema WebSocket por su cuenta.
  • Un relay por key. Si abres un segundo, el primero se cierra.

No hace falta declarar nada más: basta con poner tu URL local en webhook_url y dejarlo corriendo. La receta completa está en Conducir el ciclo.

La página de pago (/pay/:id)

Cada sesión tiene una página de pago que espeja la pantalla real: selector de método —limitado a los que el acuerdo declaró en payment_methods—, los datos que ese método pide, temporizador y clave de pago. Acepta claves mágicas que provocan cada desenlace (000000 paga; otras fallan con su motivo) y permite elegir de antemano cómo termina la fase 2. Es la vía manual, para verlo y demostrarlo; la tabla de claves está en Conducir el ciclo.

Para pruebas automatizadas, el plano de control es la vía: no necesita navegador y es reproducible.

La consola existe, y es otra cosa

Además de esta superficie programática, el simulador trae una consola con interfaz gráfica: las prácticas, la traza de eventos, el workbench y los retos. Es para mirar —depurar y aprender—, no para integrar, y por eso se documenta aparte: → El simulador desde su consola.

Todo lo que la consola muestra se puede obtener también por HTTP con el token de consola (GET /console/trace, GET /console/sessions, GET /console/progress). La consola no sabe nada que la API no cuente.

Qué emula y qué no

  • Sí: responde la API Productos validando contra la OpenAPI · fuerza paid/failed/expired y lo refleja en GET status · emite webhooks firmados con reintentos e idempotencia · acredita a dos fases · aísla por cuenta.
  • No: no mueve dinero real ni habla con bancos · no reproduce la lógica interna de SPIDI, solo el contrato.

Ocho operaciones con comportamiento, nueve con la forma

Esto es lo que más conviene saber antes de escribir una prueba contra el simulador, porque una respuesta de ejemplo se parece mucho a una de verdad:

Comportamiento real (estado, aislamiento por cuenta, webhooks)Solo la forma, con el ejemplo del contrato
login · crear acuerdo · crear partner · sesión de Botón · lote de Solicitud · consultar estado · expirar sesión · acuerdo de recepción de splitLas nueve operaciones de Paradas: listar, crear, detalle, actualizar, borrar, histórico, lote, consulta múltiple y reordenar

Las de la derecha se delatan en la respuesta: llevan la cabecera x-spidi-sim: mock, así que puedes distinguirlas sin haber leído esta página. Responden 200 con datos inventados que son idénticos para todas las cuentas — si listas paradas verás dos que tú no creaste. No las uses para probar lógica de estado: sirven para que tu deserializador vea la forma correcta de la respuesta, y para nada más. → Paradas

El porcentaje de comisión de los ejemplos es ilustrativo

El simulador reparte comisiones usando un ejemplo de 1 % en receiver_credits_summary. La comisión real la fija el banco y varía por acuerdo comercial → Límites y reglas de operación. Úsalo para comprobar que tu código lee bien el campo; no para calcular lo que vas a cobrar.

Dos parámetros más que verás en las fases: la acreditación tarda unos 5 segundos en llegar tras un pago por la página (deliberado — así ves que pagado no es recibido), y los datos de prueba caducan a los 14 días.

En qué NO se parece a producción, dicho por él mismo

Perseguir la paridad con producción sería una carrera perdida: son dos bases de código distintas. Lo que sí puede hacer un simulador honesto es saber y decir en qué no se parece, y eso es una lista finita que se mantiene.

Está publicada, y la sirve la propia instancia contra la que estás integrando:

curl https://sim-productos.abiertolab.com/fidelidad

No pide token —habla del simulador, no de datos de nadie— y viene de su código, no de un documento aparte: si una diferencia deja de ser cierta, una prueba lo tumba antes de que la leas aquí.

Hay dos clases, y conviene no confundirlas:

Huecos — SPIDI hace algo que el simulador todavía no. Su silencio no es prueba de que tu código funcione:

HuecoQué significa para ti
continue_on_error se ignoraLos lotes salen enteros o fallan enteros: el 207 de éxito parcial no se produce. Un lote a medias solo lo verás en producción
Las cabeceras X-RateLimit-* son nuestrasLas mandamos cuando el freno está encendido, pero no está medido si producción las envía — nuestra guía de errores dice «si vienen», y ese «si» sigue sin resolverse. No escribas código que dependa de que estén
Repetir nunca chocaNo se lee ninguna clave de idempotencia: repetir una llamada crea otra cosa en vez de devolverte el conflicto
El 422 solo aparece en un sitioExpirar una sesión ya pagada. El contrato lo declara en doce operaciones; aquí once no lo producen
Nueve operaciones de Paradas responden el ejemploVer arriba. Se delatan con x-spidi-sim: mock

Divergencias — lo hace distinto a propósito, y cambiarlo sería peor:

DivergenciaPor qué
Un recurso de otra cuenta responde 404, no 403Un 403 confirmaría que el recurso existe, y con una lista de identificadores eso permite averiguar cuáles son de otro. Preferimos no revelarlo aunque el contrato lo permita. Y por lo mismo ese 404 no lleva ninguna cabecera que lo distinga de uno inexistente: marcarlo sería reabrir la fuga por la puerta de atrás
El freno por exceso de peticiones no viene puesto: lo enciendes túUn 429 permanente rompería recetas, capturas, prácticas y tus propias pruebas de forma intermitente. Apagado no molesta, y encendido sirve para lo que hacía falta: ensayar tu backoff. Se enciende en Credenciales, con el límite por minuto que quieras, y vale solo para tu cuenta. Mientras esté encendido, el API responde 429 con Retry-After y X-RateLimit-* — y no alcanza a /console/**, para que no te quedes sin poder leer la traza ni volver a apagarlo
POST /api/spidipagos/refresh-token existe aquí y no está en el contratoEl entorno real lo tiene y su OpenAPI no lo declara. Sin él, quien siga la documentación de producción se daría contra un 404 y creería que se equivocó. Se delata con x-spidi-sim: fuera-de-contrato
Antes de saltar al sandbox

Esta lista es justo lo que conviene repasar al pasar del simulador al sandbox: es donde están las sorpresas. Lo que aquí figura como hueco, allí ocurre.

Siguiente paso

¿Vas a integrar? → Recibe tu primer pago, que corre contra este simulador de punta a punta.

¿Vas a probar en serio desde tu código? → Credenciales · Conducir el ciclo · Observar y depurar.


¿Contribuyes al simulador? También puede correrse localmente (MySQL 8.4 + npm run dev, o docker compose up) para desarrollo y CI. Eso vive en el repositorio del proyecto (simulador/, dev-services/), no es necesario para integrar contra el sandbox hosteado.