Recibir webhooks
Recibe webhooks de Landa Pay con la librería landapay: verifica la firma con constructEvent, maneja eventos tipados de pagos y suscripciones y evita duplicados.
Registrar un endpoint
Registra una URL https pública de tu servidor en el panel, en Desarrolladores › Webhooks. Los webhooks son del proyecto: recibes los eventos de todos tus links de pago y suscripciones en el mismo endpoint, y cada endpoint recibe solo los eventos de su modo.
Copia el secreto de firma (whsec_…) del endpoint y guárdalo en LANDAPAY_WEBHOOK_SECRET.
Recibir y verificar
landapay.webhooks.constructEvent verifica el header Landa-Signature con tu secreto, descarta los envíos con más de 5 minutos y te devuelve el evento tipado. Pásale el cuerpo crudo de la petición, no el JSON ya parseado.
import express from "express";import { SignatureVerificationError, landapay } from "landapay"; const app = express(); // Cuerpo crudo: la firma se calcula sobre los bytes recibidosapp.post("/webhooks/landapay", express.raw({ type: "application/json" }), async (req, res) => { let event; try { event = landapay.webhooks.constructEvent(req.body, req.get("Landa-Signature")); } catch (error) { if (error instanceof SignatureVerificationError) return res.status(400).send(error.message); throw error; } switch (event.type) { case "payment.approved": { const payment = event.data.object; const order = await findOrder(payment.paymentLink); // amount y order.total van en centavos de la moneda del link if (payment.amount >= order.total) { await markOrderPaid(payment.paymentLink, payment.id); } break; } case "subscription.created": case "subscription.activated": case "subscription.past_due": case "subscription.canceled": case "subscription.completed": { const subscription = event.data.object; const allowed = subscription.status === "active" || subscription.status === "trialing"; if (allowed) await grantAccess(subscription.externalId); else await revokeAccess(subscription.externalId); break; } } res.sendStatus(200);});El acceso de una suscripción vale solo con active o trialing. Quítalo en subscription.past_due. El detalle de unpaid está en Cobrar suscripciones.
// app/api/webhooks/landapay/route.tsimport { landapay } from "landapay"; export async function POST(request: Request) { const payload = await request.text(); const event = landapay.webhooks.constructEvent( payload, request.headers.get("Landa-Signature"), ); if (event.type === "payment.approved") { // event.data.object es un Payment } return new Response(null, { status: 200 });}case, TypeScript sabe si event.data.object es un Payment o una Subscription. Los campos llegan en camelCase, igual que en el resto de la librería.Tipos de evento
| Evento | Cuándo ocurre |
|---|---|
payment.approved | El pago fue aprobado; es el único que confirma un cobro |
payment.declined | La entidad rechazó el pago |
payment.error | El pago terminó con error en la pasarela |
payment.voided | Un pago aprobado fue anulado |
payment.refunded | Un pago aprobado fue reembolsado |
subscription.created | El cliente completó el checkout y se creó la suscripción |
subscription.activated | La suscripción quedó activa: primer cobro aprobado, fin de la prueba o recuperación de un cobro fallido |
subscription.past_due | Falló el cobro de renovación y la suscripción quedó en mora |
subscription.cancel_scheduled | Se programó la cancelación para el final del periodo |
subscription.resumed | Se quitó la cancelación programada |
subscription.canceled | La suscripción terminó por cancelación |
subscription.completed | La suscripción terminó al cumplir los ciclos del plan |
subscription.plan_change_scheduled | Se programó un cambio de plan para el final del periodo |
subscription.plan_changed | La suscripción pasó al plan nuevo |
subscription.plan_change_failed | El cobro del cambio de plan fue rechazado; la suscripción sigue en su plan |
Revisa los campos del evento en la referencia de webhooks.
Respuestas y reintentos
Responde con un estado 2xx en menos de 10 segundos. Si no, reintentamos el envío con esta espera desde el intento anterior:
| Reintento | Espera |
|---|---|
| 1 | 1 minuto |
| 2 | 5 minutos |
| 3 | 30 minutos |
| 4 | 2 horas |
| 5 | 6 horas |
| 6 | 12 horas |
| 7 | 24 horas |
Después la entrega queda fallida y puedes reenviarla desde el panel, donde ves cada entrega con el cuerpo enviado y la respuesta de tu servidor.
Evitar duplicados
Un mismo evento puede llegar más de una vez
id, que también viene en el header Landa-Event-Id, para no procesarlo dos veces.const alreadyProcessed = await db.webhookEvents.exists(event.id);if (alreadyProcessed) return res.sendStatus(200); await handleEvent(event);await db.webhookEvents.insert(event.id);Probar en local
Con generateTestHeader firmas un evento de prueba con tu secreto y lo envías a tu servidor local, sin esperar un pago real.
const payload = JSON.stringify({ id: "evt_TEST", object: "event", type: "payment.approved", livemode: false, created_at: new Date().toISOString(), data: { object: { id: "pay_TEST", object: "payment", status: "approved" } },}); await fetch("http://localhost:4000/webhooks/landapay", { method: "POST", headers: { "Content-Type": "application/json", "Landa-Signature": landapay.webhooks.generateTestHeader(payload), }, body: payload,});