Saltar al contenido principal

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.

Transacción a dos fasesGuion: .docx · .yaml

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.

Un aviso de acreditación por cada parte del acuerdo

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 estoUsa
Decidir si entregaspaid (fase 1)
Cuadrar contra tu estado de cuentaaccredited (fase 2) y su bank_reference_idcompuesto: 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.
En el simulador son 5 segundos

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.

El status nunca refleja la fase 2

Tras 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.

Guarda el session_id de cada transacción

Es 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:

CanalDónde está
Webhookdata.receiver_credits
GET statusdata.session_payment.receiver_credits

Un parser escrito para uno devuelve null con el otro. Molesta, pero la ves.

La que NO se ve: el identificador del receptor cambia de valor

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

Cuadra el número de créditos contra los receptores que pediste

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.

Usar el simulador · Manejar notificaciones