Skip to main content
LuaWebhook defines an HTTP endpoint that runs your execute function outside any conversation and returns its result to the caller. Once pushed and deployed, the webhook answers POST https://webhook.heylua.ai/<agentId>/<name>; the webhook ID works in place of the name. A webhook can also subscribe to platform events, which the platform delivers to execute without passing through the public URL. To have the model react to an event instead of your code, use a trigger. Verified against lua-cli 3.33.0.

Quick example

src/webhooks/OrderPaidWebhook.ts
Run it locally with a JSON object holding any of query, headers, and body.
Output

Constructor

string
required
Server-side identifier and the last segment of the URL. Lowercase with hyphens; lua compile warns Webhook name should be URL-safe (lowercase, hyphens only) otherwise. Also --name for lua push webhook and --webhook-name for lua webhooks.
string
required
One or two sentences shown in listings.
ZodType
Zod schema describing the query string. The platform doesn’t apply it; only the unit-test method execute(query, headers, body) does.
ZodType
Zod schema describing the request headers, with lowercase names as keys. Not applied by the platform.
ZodType
Zod schema describing the body. Not applied by the platform: event.body arrives unvalidated, so call bodySchema.safeParse(event.body) in execute to refuse a malformed request.
string
HMAC-SHA256 signing key. When set, every direct request must carry a valid x-lua-signature header (see Signing). It must be a string literal or a constant the compiler can resolve. Rotate it by changing the value and deploying again; set '' to turn signing off on the next push.
(event: LuaWebhookEvent) => Promise<any>
required
Your handler. For a direct request its return value is serialized as the JSON response body. No end user is in scope: call User.get(userId) with an ID from the payload, or send with Channels.send.
The constructor throws when name is empty or blank: LuaWebhook requires a non-empty `name` (used as the server-side identifier). lua compile fails with Webhook must have an execute function when execute is missing, and with Webhook `secret` must be a string literal or a compile-time-resolvable constant (a runtime expression such as `process.env.X` cannot be read at compile time) when it can’t read the secret.

Event

execute receives one LuaWebhookEvent. The type marks query, headers, and body optional; timestamp is always set.
Record<string, any>
Parsed query string; {} when there is none.
Record<string, any>
Request headers with lowercase keys.
any
Parsed JSON body as sent; bodySchema isn’t applied to it. For a subscribed platform event it is { eventType, ...payload }.
string
ISO 8601 time the request was received.
{ eventId: string; executionId: string; attempt: number }
Set on every deployed run for a subscribed platform event, absent for a direct request, and not on the type. eventId is stable across retries of one event, executionId changes per attempt, and attempt starts at 1.
Deployed runs only. event.execution is undefined under lua test webhook; read it through a widened type, (event as typeof event & { execution?: { eventId: string; executionId: string; attempt: number } }).execution.

Signing

When secret is set, the platform rejects a direct request before running any code unless it carries the signature header.
The digest is computed over the exact bytes the caller sent and compared in constant time. A missing header, a missing body, a malformed value, or a mismatch each return 401 with Invalid webhook signature. Sign the bytes you send: re-serializing JSON changes key order and whitespace and invalidates the signature. Subscribed platform events don’t pass through the public URL and aren’t signed.

Delivery

The retry policy is the platform’s; there is no per-webhook setting. event.execution.eventId stays the same across the attempts of one event, so write the handler to be idempotent; see About webhooks.
A subscribed event can run your handler more than once. Key side effects on event.execution.eventId so a retry is a no-op.

Event subscriptions

List the events a webhook can subscribe to, then subscribe a pushed webhook by name.
Output
For a message.* event, event.body is { eventType, ...payload } where the payload is the platform’s record of that message delivery.

Methods

getSecret()

Returns the signing key, or undefined. Never log it.
Errors — none.

getName()

Returns name.
Errors — none.

getDescription()

Returns description.
Errors — none.

execute()

Validates each part against its schema, builds the event, and runs your handler. Only your unit tests call it: the platform and lua test webhook pass the event to the compiled handler directly, so the validation errors below never occur in a deployed webhook.
Record<string, any>
Query parameters. Defaults to {}.
Record<string, any>
Headers. Defaults to {}.
any
Request body.
Returns — whatever your handler returns. Example
ErrorsQuery parameter validation failed: <ZodError>, Header validation failed: <ZodError>, or Body validation failed: <ZodError> when a schema rejects its part. This method is the only place they are raised.

Types

LuaWebhookConfig is an exported type. LuaWebhookEvent isn’t exported by name; derive it from the config.

See also