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.
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.
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/login | Sí | Autenticarte. Es contrato |
/api/v1/ext/** (17 operaciones) | Sí | Acuerdos, partners, sesiones, paradas, status. Es contrato |
/console/** | No | Alta self-service, traza de eventos, retos, limpieza |
/control/** | No | Forzar desenlaces y reiniciar tu cuenta |
/pay/:id | No | La página de pago espejo, con claves de prueba |
POST /register | No | Alta histórica, sin token de consola. Se mantiene por compatibilidad; no la uses |
GET /health | No | Sonda de vida del propio simulador |
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.
| Pieza | Abre | No 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 enmessage(leg1 inválido desde estado paid,leg2 requiere status paid). Es feo y es informativo. - Forzar
paidpor 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 fuerzaspaidy esperas elpayment_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.
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
| Cabecera | Contenido |
|---|---|
content-type | application/json |
spidi-signature | HMAC-SHA256 de spidi-timestamp + "." + el cuerpo crudo, en hexadecimal, con tu webhook_secret |
spidi-timestamp | Marca de tiempo de la sesión, en ISO 8601. Entra en la firma |
idempotency-key | Un 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.
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.
routing | Cuándo | Qué significa para ti |
|---|---|---|
loopback | La 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 |
relay | El 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 parsear | La entrega va por el relay, si lo tienes conectado |
direct | Cualquier otro host | El simulador hace el POST a tu servidor por internet |
loopback es la trampa que hay que conocerLos 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 crudo | Tal cual llegó. Con él y el spidi-timestamp de las cabeceras se reconstruye la cadena firmada; reserializar el JSON la invalida |
| El cuerpo parseado | Para leerlo cómodo |
| Las cabeceras | Incluidas 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
--keyes el token de API, no el de consola: es como el simulador sabe a qué cuenta pertenece ese relay.--mockes 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/expiredy lo refleja enGET 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 split | Las 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 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:
| Hueco | Qué significa para ti |
|---|---|
continue_on_error se ignora | Los 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 nuestras | Las 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 choca | No se lee ninguna clave de idempotencia: repetir una llamada crea otra cosa en vez de devolverte el conflicto |
El 422 solo aparece en un sitio | Expirar una sesión ya pagada. El contrato lo declara en doce operaciones; aquí once no lo producen |
| Nueve operaciones de Paradas responden el ejemplo | Ver arriba. Se delatan con x-spidi-sim: mock |
Divergencias — lo hace distinto a propósito, y cambiarlo sería peor:
| Divergencia | Por qué |
|---|---|
Un recurso de otra cuenta responde 404, no 403 | Un 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 contrato | El 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 |
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í sí 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, odocker 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.