Transacción a dos fases
Una transacción con SPIDI ocurre en dos fases. Entender eso es lo que separa una integración que funciona de una que parece funcionar.
Las dos fases
Fase 1 — El pago
Tu cliente paga y el dinero sale de sus manos. La sesión pasa de pending a uno de tres desenlaces:
paid— el pago salió bien.failed— fue rechazado (solo aplica al Botón).expired— la sesión venció sin pagarse.
Se emite el webhook payment_session.paid.
Dónde está el dinero en este punto, que es lo que hay que retener: no queda en poder de SPIDI. Entra en una cuenta propia del banco, y el banco es su custodio. Tu cliente ya no lo controla y no puede echarse atrás.
Fase 2 — La acreditación
El dinero se mueve desde esa cuenta del banco hasta la tuya — o hasta tantas cuentas como hayas pedido que se reparta, si el acuerdo por el que lo enrutaste se bifurca.
Aquí se aplica la comisión del banco, y por eso lo que se acredita es el monto neto. Se emite el webhook payment_session.accredited.
Llega un evento por cada transacción de crédito, uno por cada parte del acuerdo: sin split, un solo payment_session.accredited; con split, una acreditación por receptor más el cierre — todas a tu mismo webhook_url. → Split.
Puedes entregar en paid
No esperes a la acreditación para habilitar tu servicio o despachar tu producto. En cuanto la sesión llega a paid, puedes entregar.
Y el motivo es el mecanismo de arriba, no una promesa: el dinero está en un banco y el banco lo custodia. paid representa una garantía de derecho irrevocable sobre él. La fase 2 no decide si el dinero es tuyo, sino cuándo lo ves en tu cuenta — es una liquidación entre el banco y tú, no un riesgo que corras con tu cliente.
La acreditación sirve para otra cosa, y también la necesitas:
| Para esto | Usa |
|---|---|
| Decidir si entregas | paid (fase 1) |
| Cuadrar contra tu estado de cuenta | accredited (fase 2) y su bank_reference_id — compuesto: solo la parte a la izquierda del | casa con el banco |
Cuánto tarda la fase 2
Entre uno y dos minutos, no segundos. Medido en transacciones reales: el crédito entra entre 50 y 103 segundos después del pago, y el aviso llega unos 20 segundos después de que el dinero ya está.
Eso tiene dos consecuencias directas para tu código:
- Consultar el estado justo después de pagar devuelve
receiver_credits: null. No es un fallo — es que la fase 2 todavía no ocurrió. Si sondeas, insiste. - En un split, los receptores no reciben a la vez. En la medición, el partner recibió su parte 12 segundos antes que el owner. No asumas un orden ni simultaneidad.
Está puesto así a propósito, para que la secuencia completa se vea en una sola pantalla. Es didáctico, no realista: no escribas lógica que dependa de que la acreditación llegue rápido.
Cómo te enteras de cada fase
La sesión es el registro de la transacción, y las dos fases se ven ahí. Una sola consulta te da el estado completo:
GET /api/v1/ext/payment-sessions/status/{session_id}
→ data.status ← la fase 1
→ data.session_payment.receiver_credits ← la fase 2
→ data.session_payment.receiver_credits_summary
El aviso no es la única fuente: es el atajo. Te avisa en cuanto ocurre, y así no tienes que preguntar. Eso es todo lo que hace de más.
status nunca refleja la fase 2Tras paid, el status se queda en paid — la acreditación no lo cambia. No existe un estado accredited: si sondeas esperándolo, esperas para siempre.
Lo que cambia no es el status, sino que receiver_credits deja de venir null. Es otro bucle y otra condición de salida.
Y por eso importa que la sesión lo tenga todo. Si tu servidor estuvo caído, SPIDI reintenta — y luego se rinde, sin reenvío. Cuando vuelvas no habrá un aviso esperándote: habrá una sesión que puedes consultar.
session_id de cada transacciónEs tu única llave de vuelta: no hay forma de listar tus sesiones. Si lo pierdes, esa transacción es irrecuperable por API.
Las dos trampas de forma
El mismo dato viaja por los dos canales, y no viene igual.
La que se ve. receiver_credits cuelga de sitios distintos:
| Canal | Dónde está |
|---|---|
| Webhook | data.receiver_credits |
GET status | data.session_payment.receiver_credits |
Un parser escrito para uno devuelve null con el otro. Molesta, pero la ves.
split_recipient_agreement_id —el campo natural para cruzar un aviso con su receptor— devuelve dos identificadores distintos del mismo objeto según de dónde lo leas: un UUID en el webhook, el rcv_… que tú enviaste en el GET status.
Cruzar por él falla en silencio: no da error, concilia mal y no avisa. Si lees las dos fuentes, normaliza antes de cruzar.
Antes de dar por conciliado un split
receiver_credits_summary.total_credits debe coincidir con el número de receptores del reparto más el owner. Si pediste repartir a un partner y ves total_credits: 1, no concilies: el dinero puede haberse movido igual y estarías a un paso de pagarle dos veces a alguien que ya recibió su parte.
Es una comprobación de una línea y es la que detecta el caso malo.
Pruébalo
En el simulador puedes forzar cada fase por separado y ver los dos webhooks:
# fase 1 (el pago)
curl -X POST https://sim-productos.abiertolab.com/control/sessions/<id>/outcome \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" -d '{"leg":1,"outcome":"paid"}'
# fase 2 (la acreditación)
curl -X POST https://sim-productos.abiertolab.com/control/sessions/<id>/outcome \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" -d '{"leg":2,"outcome":"accredited"}'
El campo se llama leg en el plano de control: es el nombre del API para lo que aquí llamamos fase.