Saltar al contenido principal

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.

La redirección no es una confirmación de pago

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.

Success URL: verifica el status desde tu backendGuion: .docx · .yaml

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 status te sirve de respaldo (al volver a success_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_originLa sesión nació de
buttonun Botón
requestuna 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.

No lo uses tal cual: viene compuesto

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));
En el webhook la ruta es otra

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

Guárdalo cuando llegue la acreditación

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.