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

# Webhooks

> HTTP endpoints your code handles, how Lua delivers requests and platform events to them, and how they differ from triggers

A webhook is an HTTP endpoint on your agent that runs your code when something happens outside a conversation: a payment settles, an order ships, a ticket changes state. It exists because a [tool](/concepts/skills-and-tools) runs only when the model calls it mid-conversation, and many events have no conversation to belong to.

## How a webhook handles a request

You define a webhook with `new LuaWebhook({ name, description, execute })` and register it on the [agent](/concepts/agents)'s `webhooks` array. After `lua push webhook` and a deploy, Lua serves it at `https://webhook.heylua.ai/<agentId>/<webhookId>`, and also at `https://webhook.heylua.ai/<agentId>/<webhook-name>`; `lua webhooks view` lists both identifiers.

A `POST` to that URL calls `execute` with one event object, `{ query, headers, body, timestamp }`, exactly as the request arrived. The optional zod schemas (`querySchema`, `headerSchema`, `bodySchema`) describe the shape you expect; the platform doesn't apply them, so a request they would reject still reaches `execute`, and you refuse it there with `schema.safeParse`. Whatever `execute` returns is the HTTP response body, with status 200; a handler that throws answers 500 `Internal server error`, and your error text goes to `lua logs`, not to the caller. The request is handled synchronously and Lua never retries it; the caller receives your return value and applies its own retry policy, as Stripe, Shopify, and GitHub all do.

```ts src/webhooks/OrderShippedWebhook.ts theme={null}
import { LuaWebhook, User } from 'lua-cli';

export default new LuaWebhook({
  name: 'order-shipped',
  description: 'Tell the customer their order is on its way',
  async execute({ body }) {
    const user = await User.get(body.customerId);
    if (!user) return { received: true, notified: false };
    await user.send([{ type: 'text', text: `Order ${body.orderNumber} has shipped.` }]);
    return { received: true, notified: true };
  },
});
```

A webhook runs with no conversational end user, so `User.get()` with no argument has nobody to return. Store the Lua user ID in the external system when you create the order or payment, and pass it to `User.get(userId)` in the handler. From there the handler can send a message, change stored data, start a [workflow](/concepts/workflows), or hand the event to the model with `Agents.invoke`. The [execution contexts](/concepts/execution-contexts) page lists what else is available.

## Verifying who is calling

A webhook URL is public. Set `secret` on the definition and Lua runs `execute` only for requests that carry `x-lua-signature: sha256=<hex>`, the HMAC-SHA256 of the raw request body keyed with that secret. A missing, malformed, or wrong signature is answered with 401 `Invalid webhook signature` and your code never runs. The secret must be a string literal or a constant the compiler can read; `lua compile` fails on a runtime expression rather than deploy an unsigned webhook. Vendors that sign with their own scheme (Stripe's `stripe-signature`, GitHub's `x-hub-signature-256`) never send `x-lua-signature`, so leave `secret` unset for them and verify their header inside `execute`. The [LuaWebhook reference](/reference/sdk/luawebhook) has the full signing detail and a sender example.

## Platform event subscriptions

A webhook can also receive events from Lua itself. `lua webhooks subscribe --webhook-name <name> --event message.delivered` subscribes it to a WhatsApp delivery receipt; the subscribable events are `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.failed`, and `message.played` (`lua webhooks list-events` prints them). Lua calls `execute` with `query` and `headers` empty and the receipt in `body`, with its `eventType` as the first field.

Lua delivers a subscribed event from a durable queue, at least once: after a failed attempt the platform tries again until it has made 3 attempts in total, 60 seconds apart, and `event.execution.eventId` stays the same across them. Write the handler to be idempotent: key side effects on that event ID, or on the message ID in the payload, rather than on "this call happened". An event whose envelope exceeds 50,000 characters runs once and is never retried.

## Webhooks, triggers, and integration webhooks

|                 | Webhook                          | [Trigger](/concepts/triggers)                  | [Integration webhook](/concepts/integrations)                                                                   |
| --------------- | -------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| What calls it   | Any system, at a URL you give it | Any system, at a URL Lua prints                | A connected SaaS (Linear, HubSpot) on an event you subscribe to                                                 |
| What runs       | Your `execute` function          | No code of yours; Lua shapes the event         | Whatever the subscription points at                                                                             |
| What it ends in | Your response body               | A model turn, one tool call, or a workflow run | A background agent turn on channel `integration:<type>`, or a webhook or trigger URL you pass with `--hook-url` |
| Manage with     | `lua webhooks`                   | `lua triggers`                                 | `lua integrations webhooks`                                                                                     |

Pick a webhook when you need to control the response or do work without involving the model. Pick a trigger when the event should wake the agent and nothing more. Pick an integration webhook when the source is one of Lua's connected integrations, so you don't have to register a URL with the vendor yourself.

## When to use a webhook

* An external system must notify the agent, and you need to respond to it, transform its payload, or update state: webhook.
* The event should become a message to the agent, a single tool call, or a workflow start, with no custom code: [trigger](/concepts/triggers).
* The work recurs on a schedule rather than on an event: [job](/concepts/jobs).
* The work is multi-step, long-running, or needs approval: let the webhook call `Workflows.start` and return.

## Limits

* Lua budgets 180 seconds for a handler; there is no per-webhook timeout setting. The caller is waiting, so hand long work to a job or a workflow and return.
* Lua does not retry direct requests. A handler that throws answers 500.
* A deactivated webhook (`lua webhooks deactivate --webhook-name <name>`) answers 404 `Webhook not found`, the same as one that doesn't exist.
* `secret` must be a compile-time literal. Setting it to `''` and pushing removes signing.
* Only the six `message.*` events are subscribable; `delivered`, `read`, `failed`, and `played` are WhatsApp status receipts.

## Next steps

<Columns cols={2}>
  <Card title="Handle a webhook" href="/build/handle-a-webhook">Define, sign, test, and release one.</Card>
  <Card title="LuaWebhook reference" href="/reference/sdk/luawebhook">Schemas, the event object, signing, and errors.</Card>
  <Card title="lua webhooks" href="/reference/cli/webhooks">View, activate, subscribe, and deploy.</Card>
  <Card title="Triggers" href="/concepts/triggers">The no-code alternative that wakes the agent.</Card>
</Columns>
