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

# Webhooks

> Verifica la firma de webhooks de Stripe, Shopify y Meta con una línea: ctx.verifyStripe, verifyShopify, verifyMeta y verifyHmac.

Cuando tu función recibe webhooks de un servicio externo, cualquiera que conozca la URL puede enviarle datos falsos. La verificación de firma confirma que el evento viene realmente del servicio.

`ctx.verify*` hace esa verificación en una línea: lee la cabecera de firma, resuelve el secret desde tus [secrets](/guides/functions/secrets) y lanza un error si no coincide.

## Stripe

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

export default define({
  name: "stripe-webhook",
  description: "Recibe eventos de Stripe",
  input: z.object({}).passthrough(),
  config: {
    public: true,
    path: "/webhooks/stripe",
    methods: ["POST"],
    mcp: false,
  },
  async handler(event, ctx, request) {
    await ctx.verifyStripe(request);

    ctx.log("Evento verificado", { tipo: event.type });

    if (event.type === "payment_intent.succeeded") {
      await ctx.jelou.send({
        type: "text",
        to: event.data.object.metadata.telefono,
        text: "¡Recibimos tu pago!",
      });
    }

    return { received: true };
  },
});
```

Configura el secret una sola vez:

```bash theme={null}
jelou functions secrets set stripe-webhook STRIPE_WEBHOOK_SECRET=whsec_...
```

<Note>
  El `event` que recibe tu handler ya es el cuerpo validado con Zod. La plataforma consumió el cuerpo del request para validarlo, así que llamar a `request.json()` dentro del handler lanza un error — usa el primer parámetro.
</Note>

## Proveedores soportados

| Verificador                     | Cabecera                | Secret                                    |
| ------------------------------- | ----------------------- | ----------------------------------------- |
| `ctx.verifyStripe(request)`     | `stripe-signature`      | `STRIPE_WEBHOOK_SECRET`                   |
| `ctx.verifyShopify(request)`    | `x-shopify-hmac-sha256` | `SHOPIFY_WEBHOOK_SECRET`                  |
| `ctx.verifyMeta(request)`       | `x-hub-signature-256`   | `META_WEBHOOK_SECRET` o `META_APP_SECRET` |
| `ctx.verifyHmac(request, opts)` | La que indiques         | `opts.secretEnv` o `opts.secret`          |

Stripe además rechaza eventos con más de 5 minutos de antigüedad, lo que impide que alguien reenvíe un evento antiguo capturado.

## Otros servicios

Para Twilio, GitHub, Slack o cualquier servicio con firma HMAC-SHA256, usa `ctx.verifyHmac`. La cabecera es obligatoria porque no hay un valor por defecto:

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

## Rotar secrets

Durante una rotación, acepta el secret viejo y el nuevo a la vez separándolos por coma:

```bash theme={null}
jelou functions secrets set mi-webhook STRIPE_WEBHOOK_SECRET=whsec_nuevo,whsec_viejo
```

Cuando confirmes que el proveedor ya usa el nuevo, vuelve a dejar uno solo.

## Manejar el fallo

Los verificadores lanzan `WebhookVerificationError` con un código que indica qué pasó:

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

async handler(event, ctx, request) {
  try {
    await ctx.verifyStripe(request);
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      ctx.log("Firma rechazada", { code: err.code, provider: err.provider });
      return new Response(null, { status: 401 });
    }
    throw err;
  }

  return { received: true };
}
```

| Código              | Significado                                |
| ------------------- | ------------------------------------------ |
| `missing_signature` | El request llegó sin cabecera de firma     |
| `invalid_signature` | La firma no coincide con el cuerpo         |
| `expired_timestamp` | El evento es demasiado antiguo (Stripe)    |
| `missing_secret`    | No configuraste el secret de ese proveedor |

<Tip>
  Si no capturas el error, la función responde con error y el proveedor reintentará el webhook. Para Stripe y Shopify eso suele ser lo correcto solo cuando el fallo es temporal; ante una firma inválida es mejor responder `401` y no reintentar.
</Tip>

## Funciones públicas

Los webhooks necesitan `config.public: true` para que el servicio externo pueda llamarlos sin credenciales de Jelou. Esa es justamente la razón por la que la verificación de firma es obligatoria: es el único control de acceso que queda.

```typescript theme={null}
config: {
  public: true,      // el proveedor no tiene API key de Jelou
  methods: ["POST"], // los webhooks siempre son POST
  mcp: false,        // no tiene sentido como herramienta de IA
}
```

Ver [funciones públicas](/guides/functions/public).

## Testing

En los tests, `createMockContext()` deja cada verificador lanzando `missing_secret`. Para probar el resto del handler, desactiva la verificación:

```typescript theme={null}
import { createMockContext, createMockWebhookVerifiers } from "@jelou/functions/testing";

const verifiers = createMockWebhookVerifiers({ stripe: true });

const ctx = createMockContext({
  verifyStripe: verifiers.stripe,
});

// Después de ejecutar el handler puedes revisar las llamadas registradas
verifiers.calls; // [{ method: "stripe", ... }]
```

Ver la [guía de testing](/guides/functions/testing).

<CardGroup cols={2}>
  <Card title="Funciones públicas" icon="globe" href="/guides/functions/public">
    Recibir peticiones sin credenciales de Jelou.
  </Card>

  <Card title="Secrets" icon="key" href="/guides/functions/secrets">
    Guardar los secrets de cada proveedor.
  </Card>

  <Card title="Ejecuciones diferidas" icon="clock" href="/guides/functions/diferidas">
    Agendar un seguimiento al recibir el webhook.
  </Card>

  <Card title="Receptor de webhooks" icon="webhook" href="/guides/functions/ejemplo-webhook">
    Ejemplo completo listo para copiar.
  </Card>
</CardGroup>
