Saltar al contenido principal

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.

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

Next.js (App Router)TypeScript
// 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 });}
Dentro de cada 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

EventoCuándo ocurre
payment.approvedEl pago fue aprobado; es el único que confirma un cobro
payment.declinedLa entidad rechazó el pago
payment.errorEl pago terminó con error en la pasarela
payment.voidedUn pago aprobado fue anulado
payment.refundedUn pago aprobado fue reembolsado
subscription.createdEl cliente completó el checkout y se creó la suscripción
subscription.activatedLa suscripción quedó activa: primer cobro aprobado, fin de la prueba o recuperación de un cobro fallido
subscription.past_dueFalló el cobro de renovación y la suscripción quedó en mora
subscription.cancel_scheduledSe programó la cancelación para el final del periodo
subscription.resumedSe quitó la cancelación programada
subscription.canceledLa suscripción terminó por cancelación
subscription.completedLa suscripción terminó al cumplir los ciclos del plan
subscription.plan_change_scheduledSe programó un cambio de plan para el final del periodo
subscription.plan_changedLa suscripción pasó al plan nuevo
subscription.plan_change_failedEl 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:

ReintentoEspera
11 minuto
25 minutos
330 minutos
42 horas
56 horas
612 horas
724 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

Usa su id, que también viene en el header Landa-Event-Id, para no procesarlo dos veces.
Procesar cada evento una vezTypeScript
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.

Enviar un webhook de pruebaTypeScript
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,});