Success URL: verificar status server-side
Cuando la persona termina, SPIDI la redirige a tu success_url. Es tentador marcar el pedido como pagado ahí mismo. No lo hagas.
Llegar a success_url solo dice que la persona volvió a tu sitio. No prueba que el pago se completó: alguien podría abrir esa URL directamente, o volver sin haber pagado. Confirma siempre desde tu backend.
Las dos formas correctas de confirmar
1. Consultar el status (pull)
Desde tu backend (no desde el navegador), consulta el estado real:
curl https://sim-productos.abiertolab.com/api/v1/ext/payment-sessions/status/<session_id> \
-H "Authorization: Bearer <token>" # -> { "data": { "status": "paid" } }
Marca el pedido como pagado solo si status es paid.
2. Escuchar el webhook (push)
Procesa el aviso payment_session.paid que SPIDI envía a tu webhook_url (firmado). Es la vía más fiable y no depende de que la persona vuelva. → Manejar notificaciones.
Lo ideal es combinar ambas: el webhook te avisa en cuanto ocurre; el
GET statuste sirve de respaldo (al volver asuccess_url, o como reconciliación periódica).
Un solo endpoint, nazca como nazca la sesión
Una sesión de pago puede nacer de dos sitios —un Botón o una Solicitud—, y cada uno tiene su propio endpoint de creación. Para consultarla no: se usa el mismo endpoint, sin importar de dónde vino.
Y la respuesta te dice cuál fue, en session_origin:
session_origin | La sesión nació de |
|---|---|
button | un Botón |
request | una Solicitud |
Por qué te importa si tu sistema crea de las dos formas: el mismo código de confirmación te
sirve para todo, y session_origin es lo que te permite decidir qué hacer después sin
guardarte esa correspondencia por tu cuenta. Si despachas un pedido para un Botón y envías un
recibo para una Solicitud, ahí está el dato.
Patrón recomendado (en tu success_url)
// handler de tu success_url — en el SERVIDOR
app.get("/pago-exitoso", async (req, res) => {
const r = await fetch(`${SPIDI}/api/v1/ext/payment-sessions/status/${req.query.session_id}`,
{ headers: { Authorization: `Bearer ${TOKEN}` } });
const { data } = await r.json();
if (data.status === "paid") marcarPedidoPagado(req.query.session_id);
else mostrarPendiente(); // no asumas pago
});
Qué mostrarle a tu cliente en esa pantalla
Dos datos que ya te llegan y conviene enseñar, porque son lo que tu cliente te va a pedir después:
spidi_transaction_id— el número de la transacción en SPIDI. Muéstralo: es la referencia con la que se resuelve cualquier consulta posterior.spidi_transaction_url— el enlace al comprobante oficial de SPIDI. Un botón hacia ahí le evita a tu cliente escribirte para pedirlo.
Si no configuras success_url, el pagador no se queda en el aire: SPIDI muestra su propia pantalla de confirmación con los detalles del pago. Tener la tuya sirve para que la experiencia no salga de tu sitio, no para que exista.
Conciliar contra tu estado de cuenta
Cuando revises el banco vas a ver la acreditación, no el débito — y para casarla necesitas el número que usa el banco, no el de SPIDI.
Está en el endpoint de status, dentro de los detalles del crédito: bank_reference_id.
El campo llega como <referencia>|<comisión> — por ejemplo "8852|0.50". Solo la parte
izquierda del pipe es la referencia que aparece en tu extracto; la derecha repite la comisión
bancaria de ese crédito, y puede venir 0.00 si se descontó por separado.
Si casas el valor entero contra tu banco, no cuadra ninguna línea, nunca. Y como la parte
izquierda coincide con el spidi_credit_id del mismo bloque, tienes con qué comprobarlo.
const { data } = await (await fetch(
`${SPIDI}/api/v1/ext/payment-sessions/status/${sessionId}`,
{ headers: { Authorization: `Bearer ${TOKEN}` } })).json();
// OJO a la ruta: en GET status los créditos cuelgan de session_payment,
// y receiver_credits es un OBJETO con owner y partners, no un arreglo.
const creditos = data.session_payment?.receiver_credits;
// El campo viene compuesto: "<referencia>|<comisión>". Para el banco solo vale la izquierda.
const soloReferencia = (v) => String(v ?? "").split("|")[0];
// la referencia con la que casas la línea de tu extracto bancario
const referenciaBanco = soloReferencia(creditos?.owner?.bank_reference_id);
// si la sesión llevaba split, cada receptor trae la suya
const referenciasPartners = (creditos?.partners ?? []).map((p) => soloReferencia(p.bank_reference_id));
El mismo dato llega en payment_session.accredited como data.receiver_credits, al mismo nivel que session_payment y no dentro de él. Si lees las dos fuentes con el mismo código, una de las dos te devolverá undefined sin error. → Transacción a dos fases
El bank_reference_id nace con el fase 2. Si tu conciliación es mensual, persístelo al recibir payment_session.accredited y te ahorras consultar sesión por sesión.
Recordatorio: el dinero confirmado es la fase 2
paid confirma el débito. Que te llegue el dinero es la acreditación (fase 2), y la conoces por dos vías: el webhook payment_session.accredited, o receiver_credits en la respuesta del status — la misma que acabas de usar aquí arriba. → Transacción a dos fases.