> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jelou.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Receptor de webhooks

> Recibe callbacks POST de servicios externos con validación de payload, ruta personalizada y procesamiento de eventos.

Recibes callbacks POST de cualquier servicio externo (pasarelas de pago, GitHub, CRMs, etc.), validas el payload y procesas el evento.

**Patrón:** ruta personalizada + solo POST + MCP desactivado + validación de payload.

```typescript index.ts theme={null}
import { define, z } from "@jelou/functions";

export default define({
  name: "webhook-receiver",
  description: "Recibe y procesa webhooks de servicios externos",
  input: z.object({
    event: z.string(),
    data: z.object({
      id: z.string(),
      status: z.string(),
      metadata: z.record(z.unknown()).optional(),
    }),
  }),
  config: {
    public: true,
    path: "/webhooks/events",
    methods: ["POST"],
    mcp: false,
  },
  handler: async (input, ctx) => {
    ctx.log("Webhook recibido", {
      event: input.event,
      id: input.data.id,
      requestId: ctx.requestId,
    });

    const secret = ctx.env.get("WEBHOOK_SECRET");

    switch (input.event) {
      case "payment.completed": {
        ctx.log("Pago completado", { id: input.data.id });
        return { acknowledged: true, action: "payment_processed" };
      }
      case "user.created": {
        ctx.log("Usuario creado", { id: input.data.id });
        return { acknowledged: true, action: "user_synced" };
      }
      default: {
        ctx.log("Evento no manejado", { event: input.event });
        return { acknowledged: true, action: "ignored" };
      }
    }
  },
});
```

## Prueba local

```bash theme={null}
curl -X POST http://localhost:3000/webhooks/events \
  -H "Content-Type: application/json" \
  -d '{"event": "payment.completed", "data": {"id": "pay_123", "status": "success"}}'
```

## Por qué funciona así

<Tip>
  * `config.methods: ["POST"]` — rechaza GET, PUT, etc. Los webhooks siempre son POST.
  * `config.mcp: false` — no tiene sentido exponer un webhook como herramienta de IA.
  * `config.path` — ruta fija que configuras en el servicio externo.
  * El esquema `input` valida la estructura del payload antes de que llegue al handler.
</Tip>

## Validar firma del webhook

Una función pública puede ser llamada por cualquiera que conozca la URL. En producción, verifica la firma del servicio antes de procesar el evento — `ctx.verify*` lo hace en una línea:

```typescript theme={null}
handler: async (input, ctx, request) => {
  await ctx.verifyHmac(request, {
    secretEnv: "WEBHOOK_SECRET",
    header: "x-webhook-signature",
  });

  // Procesar el evento...
  return { acknowledged: true };
}
```

Si la firma no coincide, lanza un error y el evento no llega a tu lógica.

Para Stripe, Shopify y Meta usa el verificador de cada proveedor, que ya conoce su cabecera y su secret:

```typescript theme={null}
await ctx.verifyStripe(request);
await ctx.verifyShopify(request);
await ctx.verifyMeta(request);
```

<Warning>
  Configura el secret antes de desplegar: `jelou functions secrets set mi-webhook WEBHOOK_SECRET=whsec_...`
</Warning>

<Card title="Guía de webhooks" icon="shield-check" href="/guides/functions/webhooks">
  Proveedores soportados, rotación de secrets, códigos de error y testing.
</Card>
