- A project created with
lua initand signed in withlua auth configure(Install and sign in). - The sender’s payload shape and the id it puts on each event.
1
Define the webhook
LuaWebhook takes a kebab-case name, an optional secret, optional Zod schemas for body, headers, and query, and execute. The schemas are not enforced and the event’s body is typed any: a body that fails bodySchema still reaches execute, so keep the schema in a const and safeParse it first. The handler receives one event with body, headers (lowercase keys), query, and timestamp; whatever it returns is the response body with status 200, and a throw answers 500.src/webhooks/PaymentSettledWebhook.ts
openssl rand -hex 16); the one printed here is public. It is the one secret that lives in source: committed with your code, stored in every source backup, and frozen into any agent template you publish, where the SECRET_DETECTED lint may refuse it. Rotate it by changing the constant and deploying. With secret set, the platform runs execute only for requests whose x-lua-signature header is sha256= plus the hex HMAC-SHA256 of the raw body keyed with that secret; a request without the header, or with a wrong one, gets 401 Invalid webhook signature before your code runs. So secret is for callers you control: a third-party sender that signs with its own scheme (Zendesk, Stripe, GitHub) can’t reach a webhook with secret set. Leave it unset for those and check a token inside execute, for example headers.authorization against `Bearer ${env('WEBHOOK_TOKEN')}`, as the customer support example does.2
Make the handler idempotent
Senders retry until they get a 2xx, so the same event can arrive twice. Key every side effect on the sender’s event id: the handler records it in a A webhook has no end user:
Data collection before doing the work, and a repeat answers 200 with duplicate: true. Lua never retries a direct request but delivers subscribed platform events at least once, 3 attempts 60 seconds apart, with the same event.execution.eventId each time.Deployed runs only.
event.execution is undefined under lua test webhook and absent from the type; see the LuaWebhook reference.User.get() needs a stored id (Identify users).3
Register it and run it locally
Add the webhook to Run it again and the output is
LuaAgent.webhooks in src/index.ts, then run it with a JSON object holding any of query, headers, and body. The local run skips the signature check.Output
{ received: true, duplicate: true }: the local run wrote to the real webhook-events collection, shared by the sandbox and production. A body without orderNumber returns { received: false, error: 'Body does not match the schema' }.4
Release
Push uploads a webhook version and changes nothing for callers. To release it with other changes instead, skip the deploy: run No command prints the URL: it is
lua deploy webhook then creates and promotes an agent version scoped to this webhook, so it is live at once and appears in lua version list.lua push all --ci --force, then lua version create --ci -m "Add payment-settled webhook", which prints ✓ Created v<n> (staged). Run `lua version promote v<n>` to deploy., then lua version promote <n> with that number (<n> or v<n>, no confirmation; in a script n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version')) (Release an agent to production). Either way, lua webhooks view confirms the deployment and prints the webhook id.Output
https://webhook.heylua.ai/<agentId>/payment-settled (the webhook id works in place of the name; the agent id is agent.agentId in lua.skill.yaml).5
Verify
Sign the exact bytes you send: re-serializing the JSON changes whitespace or key order and invalidates the signature.The response is the handler’s return value,
{"received":true,"orderFound":false} for an unknown order, and lua logs --type webhook --name payment-settled --limit 5 --ci shows the run. No CLI command sends a request to a deployed webhook.Options you may need
Subscribe to platform events
A pushed webhook can also subscribe to the platform’s WhatsApp delivery events (lua webhooks list-events names them); event.body then carries eventType first, query and headers are empty, and nothing is signed.
If it isn’t working
401 Invalid webhook signature
401 Invalid webhook signature
The header is missing or lacks the
sha256= prefix, the body was re-serialized after signing, or the secret differs from the deployed version’s. Sign the string you send; deploy again after changing the secret.404 Webhook not found
404 Webhook not found
The webhook isn’t deployed, was deactivated, or the URL is wrong. Check
lua webhooks view --ci for Deployed; lua webhooks activate --webhook-name payment-settled turns it back on.lua compile fails on the secret
lua compile fails on the secret
The message reads
Webhook `secret` must be a string literal or a compile-time-resolvable constant. Replace env('…') or process.env with a literal or a const in the same file; rotate by changing the value and deploying.Next steps
LuaWebhook reference
Schemas, the event object, signing, delivery, and errors.
lua webhooks reference
View, deploy, activate, deactivate, subscribe, and delete.
Create a trigger
Wake the agent from a URL without a handler.
Send proactive messages
Message the end user from the handler.

