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

# LuaWebhook

> HTTP endpoints for receiving external events and integrations

## Overview

`LuaWebhook` allows you to create HTTP endpoints that can receive events from external services like Stripe, Shopify, GitHub, or any other webhook-enabled platform.

```typescript theme={null}
import { LuaWebhook, User } from 'lua-cli';

const paymentWebhook = new LuaWebhook({
  name: 'payment-webhook',
  description: 'Handle Stripe payment events',
  execute: async (event) => {
    const { body } = event;

    // ⚠️ Webhooks have NO conversational context
    // MUST provide userId to notify users
    
    if (body?.type === 'payment_intent.succeeded') {
      const customerId = body.data?.object?.metadata?.customerId;
      if (customerId) {
        const user = await User.get(customerId);
        await user.send([{
          type: 'text',
          text: '✅ Payment confirmed!'
        }]);
      }
    }
    return { received: true };
  }
});

export default paymentWebhook;
```

<Warning>
  **No Conversational Context:** Webhooks execute outside of user conversations. You MUST use `User.get(userId)` with an explicit userId. Always store the user ID in your payment/order metadata.
</Warning>

<Note>
  HTTP webhooks for external integrations. Use with LuaAgent.
</Note>

## Webhooks vs Triggers

A `LuaWebhook` gives you an `execute` function and full control of the HTTP response — your code does the work. A [`LuaTrigger`](/api/luatrigger) has **no** `execute` function: it declaratively verifies, filters, and transforms an incoming event, then wakes the agent to do the work itself. If the right reaction to an event is "have the agent handle it", reach for a trigger; if you need a custom response body or code-only side effects, stay here. See [LuaTrigger](/api/luatrigger) for the full comparison.

## Use Cases

<CardGroup cols={2}>
  <Card title="Payment Events" icon="credit-card">
    Stripe, PayPal webhooks for payment processing
  </Card>

  <Card title="E-commerce Updates" icon="cart-shopping">
    Shopify, WooCommerce order notifications
  </Card>

  <Card title="Git Events" icon="code-branch">
    GitHub, GitLab deployment triggers
  </Card>

  <Card title="Custom Integrations" icon="plug">
    Any service that sends HTTP webhooks
  </Card>
</CardGroup>

## User Access in Webhooks

| Context      | How to Get User      | userId Required?              |
| ------------ | -------------------- | ----------------------------- |
| **Webhooks** | `User.get(userId)`   | ✅ **YES** - Store in metadata |
| **Tools**    | `User.get()`         | ❌ No - automatic context      |
| **LuaJob**   | `User.get(userId)`   | ✅ **YES** - Store in metadata |
| **Jobs API** | `jobInstance.user()` | ❌ No - automatic context      |

<Warning>
  **Webhooks are context-less:** They're triggered by external systems, not user conversations. Always store the Lua user ID in your payment/order metadata so you can notify the right user.
</Warning>

## Constructor

### new LuaWebhook(config)

Creates a new webhook endpoint.

<ParamField path="config" type="LuaWebhookConfig" required>
  Webhook configuration object
</ParamField>

## Configuration Parameters

### Required Fields

<ParamField path="name" type="string" required>
  Unique webhook name

  **Format**: lowercase, hyphens, underscores

  **Examples**: `'payment-webhook'`, `'order-update-webhook'`
</ParamField>

<ParamField path="execute" type="function" required>
  Function that handles incoming webhook events

  **Signature:** `(event: WebhookEvent) => Promise<any>`

  ```typescript theme={null}
  execute: async (event) => {
    const { query, headers, body } = event;
    // ...
  }
  ```
</ParamField>

### Optional Fields

<ParamField path="description" type="string">
  Webhook description for documentation
</ParamField>

<ParamField path="secret" type="string">
  HMAC-SHA256 signing key. When set, every request must carry `x-lua-signature: sha256=<hex HMAC-SHA256 of the raw body>` or it is rejected with **401** before `execute` runs. Must be a literal or a build-time-resolvable constant. Rotate by changing the value and re-deploying; set to `''` to turn verification off. See [Verify Requests](/overview/webhooks#verify-requests).
</ParamField>

## WebhookEvent Shape

Every webhook receives a single object with request details:

```typescript theme={null}
interface WebhookEvent {
  query: Record<string, any>;
  headers: Record<string, any>;
  body: any;
  timestamp: string;
  execution?: {
    eventId: string;      // stable idempotency key — survives retries & redeliveries
    executionId: string;  // unique per attempt
    attempt: number;      // 1-based attempt counter
  };
}
```

Destructure the pieces you need inside your handler:\
`const { query, headers, body, timestamp } = event;`

<Note>
  `execution` is present only when your handler is invoked via a durably-delivered [platform event](#event-subscriptions). It is `undefined` for synchronous requests sent directly to your webhook URL and during local `lua test` / `lua dev` runs. See [Delivery Semantics](#delivery-semantics).
</Note>

## Complete Examples

### Stripe Payment Webhook

```typescript theme={null}
import { LuaWebhook, env, Orders, User } from 'lua-cli';

const stripeWebhook = new LuaWebhook({
  name: 'stripe-payment-webhook',
  description: 'Handle Stripe payment events',
  
  execute: async (event) => {
    const { body } = event;
    console.log('Stripe event:', body?.type);
    
    switch (body?.type) {
      case 'payment_intent.succeeded':
        const paymentIntent = body.data?.object;
        
        // Update order status
        const order = await Orders.getById(paymentIntent.metadata.orderId);
        if (order) {
          await order.updateStatus('CONFIRMED');
          
          // Get user by ID from payment metadata
          const customerId = paymentIntent.metadata.customerId;
          const user = await User.get(customerId);
          
          // Notify the specific user
          await user.send([{
            type: 'text',
            text: `✅ Payment confirmed! Order #${order.id} is being processed. Amount: $${paymentIntent.amount/100}`
          }]);
        }
        
        return { success: true, orderId: order?.id };
        
      case 'payment_intent.payment_failed':
        const failed = body.data?.object;
        console.error('Payment failed:', failed);
        
        // Notify user of failure
        const failedCustomerId = failed.metadata.customerId;
        if (failedCustomerId) {
          const user = await User.get(failedCustomerId);
          await user.send([{
            type: 'text',
            text: `❌ Payment failed: ${failed.last_payment_error?.message}. Please try again.`
          }]);
        }
        
        return { success: false, reason: failed.last_payment_error?.message };
        
      default:
        console.log('Unhandled event type:', body?.type);
        return { received: true };
    }
  }
});

export default stripeWebhook;
```

<Note>
  **Important:** Always store the user ID (customerId) in your payment metadata so webhooks can notify the correct user. Example: `metadata: { customerId: user.id, orderId: order.id }`
</Note>

### Shopify Order Webhook

```typescript theme={null}
import { LuaWebhook, env, Data, User } from 'lua-cli';

const shopifyOrderWebhook = new LuaWebhook({
  name: 'shopify-order-webhook',
  description: 'Handle Shopify order events',
  
  execute: async (event) => {
    const { body } = event;
    const order = body;
    
    // Store order in custom data
    await Data.create('shopify-orders', {
      orderId: order.id,
      orderNumber: order.order_number,
      customer: order.customer,
      total: order.total_price,
      items: order.line_items,
      status: order.financial_status,
      customerId: order.customer.id,  // Store customer ID
      createdAt: order.created_at
    }, `Order #${order.order_number} ${order.customer.email} ${order.total_price}`);
    
    // Notify specific customer using their ID
    const user = await User.get(order.customer.id);
    await user.send([{
      type: 'text',
      text: `🛍️ Order #${order.order_number} confirmed!\n\nTotal: $${order.total_price}\nItems: ${order.line_items.length}\n\nWe'll send shipping updates soon!`
    }]);
    
    return {
      success: true,
      orderId: order.id,
      orderNumber: order.order_number,
      customerId: order.customer.id
    };
  }
});

export default shopifyOrderWebhook;
```

<Note>
  **Customer Identification:** Map external customer IDs (Shopify, Stripe) to Lua user IDs. Store this mapping in your order/payment metadata for webhook notifications.
</Note>

### GitHub Deployment Webhook

```typescript theme={null}
import { LuaWebhook, env, User } from 'lua-cli';

const githubWebhook = new LuaWebhook({
  name: 'github-deployment-webhook',
  description: 'Track GitHub deployment events',
  
  execute: async (event) => {
    const { body } = event;
    const payload = body;
    
    if (payload?.action === 'deployment_status') {
      const status = payload.deployment_status;
      const deployment = payload.deployment;
      
      // Notify on deployment completion
      // Note: You need to provide userId - webhooks have no conversational context
      if (status.state === 'success') {
        // In a real scenario, you'd get userId from deployment metadata
        // const user = await User.get(userId);
        // await user.send([{
        //   type: 'text',
        //   text: `🚀 Deployment successful!\n\nEnvironment: ${deployment.environment}\nRef: ${deployment.ref}\nURL: ${status.target_url}`
        // }]);
        console.log('Deployment successful:', deployment.environment);
      } else if (status.state === 'failure') {
        console.error('Deployment failed:', deployment.environment);
      }
      
      return { success: true, state: status.state };
    }
    
    return { received: true };
  }
});

export default githubWebhook;
```

### Generic Webhook Template

```typescript theme={null}
import { LuaWebhook, env } from 'lua-cli';

const genericWebhook = new LuaWebhook({
  name: 'custom-integration-webhook',
  description: 'Handle events from custom service',
  
  execute: async (event) => {
    const { query, headers, body } = event;
    try {
      // Validate event structure
      if (!body || !body.type) {
        throw new Error('Invalid event structure');
      }
      
      // Log event for debugging
      console.log('Received webhook:', {
        type: body.type,
        timestamp: new Date().toISOString(),
        dataKeys: Object.keys(body.data || {})
      });
      
      // Process event based on type
      switch (body.type) {
        case 'resource.created':
          await handleCreate(body.data);
          break;
          
        case 'resource.updated':
          await handleUpdate(body.data);
          break;
          
        case 'resource.deleted':
          await handleDelete(body.data);
          break;
          
        default:
          console.log('Unknown event type:', body.type);
      }
      
      return { success: true, processed: body.type };
      
    } catch (error) {
      console.error('Webhook processing error:', error);
      
      return {
        success: false,
        error: error.message,
        timestamp: new Date().toISOString()
      };
    }
  }
});

async function handleCreate(data: any) {
  // Handle creation logic
  console.log('Resource created:', data.id);
}

async function handleUpdate(data: any) {
  // Handle update logic
  console.log('Resource updated:', data.id);
}

async function handleDelete(data: any) {
  // Handle deletion logic
  console.log('Resource deleted:', data.id);
}

export default genericWebhook;
```

## Event Subscriptions

Webhooks can subscribe to **platform events**: delivery status updates for every outbound channel (WhatsApp, SMS, email, Messenger, Instagram, and the rest). When subscribed, your webhook's `execute` function receives the event payload automatically.

### Available Event Types

| Event               | Trigger                                                                              |
| ------------------- | ------------------------------------------------------------------------------------ |
| `message.sent`      | The provider accepted the message                                                    |
| `message.delivered` | The message reached the recipient (channels that report delivery)                    |
| `message.read`      | The recipient read the message (channels that report reads)                          |
| `message.failed`    | The message could not be delivered; `error` carries the category and provider detail |

Channels without receipts (Slack, Teams, Front, web chat) stop at `message.sent`.

### Subscribing via CLI

```bash theme={null}
lua webhooks list-events
lua webhooks subscribe --webhook-name my-webhook --event message.delivered
lua webhooks subscribe --webhook-name my-webhook --event message.read
lua webhooks unsubscribe --webhook-name my-webhook --event message.delivered
```

### Event Payload Shape

When an event fires, your webhook receives a `WebhookEvent` where `body` is the delivery record, the same object `Channels.getStatus` returns:

```typescript theme={null}
interface DeliveryEventPayload {
  id: string;                 // deliveryId, also returned by Channels.*.send
  agentId: string;
  userId?: string;
  channel: string;            // 'whatsapp' | 'sms' | 'email' | 'messenger' | ...
  provider: string;           // 'meta' | 'vonage' | 'bird' | 'ses' | ...
  channelIdentifier: string;  // the sending phone number, inbox, or page
  recipient: string;
  providerMessageId?: string;
  messageWamid?: string;      // same as providerMessageId, kept for existing WhatsApp handlers
  conversationMessageId?: string;
  origin: 'agent_reply' | 'channels_send' | 'template' | 'system_template' | 'queued_flush' | 'admin_template' | 'notification' | 'unknown';
  templateName?: string;
  idempotencyKey?: string;
  status: 'queued' | 'accepted' | 'sent' | 'delivered' | 'read' | 'failed' | 'expired';
  error?: {
    category: 'window_closed' | 'unreachable' | 'billing' | 'opted_out' | 'throttled' | 'auth' | 'template' | 'media' | 'invalid_request' | 'compliance' | 'experiment' | 'provider' | 'unknown';
    provider: string;
    code: string;       // provider's own code, e.g. Meta 131042
    title: string;
    detail?: string;
  };
  events: Array<{ status: string; at: string; source: 'send' | 'callback' | 'sweep' }>;
  pricing?: { billable: boolean; category?: string; model?: string };
  createdAt: string;
  updatedAt: string;
  deliveredAt?: string;
  readAt?: string;
  failedAt?: string;
}
```

### Delivery Semantics

Subscribed platform events are delivered **at least once**. Each event is durably queued and processed on isolated infrastructure with automatic retries — up to **3 attempts total**, a fixed 60-second wait between attempts, retried only on failure. Your handler may therefore run **more than once** for a single logical event, so keep side effects idempotent.

Each invocation receives `event.execution`:

| Field         | Description                                                                                                                  |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `eventId`     | Stable identifier for the logical event. Unchanged across retries and redeliveries — use it as your idempotency / dedup key. |
| `executionId` | Unique identifier for this individual attempt. Not a dedup key.                                                              |
| `attempt`     | 1-based attempt counter (`1` on the first delivery, `2` on the first retry, and so on).                                      |

```typescript theme={null}
execute: async (event) => {
  const { eventId } = event.execution ?? {};

  // Key side effects on eventId so a repeated delivery is a no-op
  await Data.create('delivery-events', {
    idempotencyKey: eventId,
    status: event.body.status,
  }, `${event.body.status} ${eventId}`);

  return { received: true };
}
```

<Note>
  `event.execution` is only present for durably-delivered platform events — it is `undefined` for synchronous direct requests to your webhook URL and during local `lua test` / `lua dev` runs. Very large event payloads (over 50,000 characters, which is rare) are delivered without the at-least-once retry guarantee.
</Note>

<Warning>
  **Direct requests are unchanged.** Requests sent straight to your webhook URL are still handled **synchronously** with no automatic retries — the caller receives your handler's return value and applies its own retry policy. At-least-once delivery and `event.execution` apply only to subscribed platform events.
</Warning>

### Example: Track Delivery for Analytics

```typescript theme={null}
import { LuaWebhook, Data } from 'lua-cli';

const analyticsWebhook = new LuaWebhook({
  name: 'message-analytics',
  description: 'Track message delivery for analytics',
  
  execute: async (event) => {
    const { body } = event;
    
    await Data.create('message-analytics', {
      messageId: body.messageWamid,
      recipient: body.recipientId,
      status: body.status,
      channel: body.channel,
      timestamp: body.timestamp,
      billable: body.pricing?.billable
    }, `${body.status} ${body.recipientId}`);
    
    return { tracked: true };
  }
});

export default analyticsWebhook;
```

## Using with LuaAgent

Webhooks are added to your agent configuration:

```typescript theme={null}
import { LuaAgent } from 'lua-cli';
import stripeWebhook from './webhooks/stripe';
import shopifyWebhook from './webhooks/shopify';
import githubWebhook from './webhooks/github';

export const agent = new LuaAgent({
  name: 'my-agent',
  persona: '...',
  skills: [...],
  
  webhooks: [
    stripeWebhook,
    shopifyWebhook,
    githubWebhook
  ]
});
```

## Webhook URLs

After deploying, your webhooks can be called using either identifier:

```
https://webhook.heylua.ai/{agentId}/{webhookId}    // legacy + default
https://webhook.heylua.ai/{agentId}/{webhook-name} // friendly alias
```

**Notes:**

* `agentId` is your agent identifier (e.g., `agent_abc123`)
* `webhookId` is the UUID shown when you create the webhook
* `webhook-name` is the `name` you pass to `new LuaWebhook`
* You can copy both URLs after pushing: `lua push webhook`

**Examples:**

```
https://webhook.heylua.ai/agent_abc123/webhook_01JD3RZ9VX9W5
https://webhook.heylua.ai/agent_abc123/payment-webhook
```

Configure either URL in your external service (Stripe, Shopify, etc.)

## Testing Webhooks

### Local Testing

```bash theme={null}
lua test
# Select: Webhook → your-webhook-name
# Provide test payload
```

### Test Payloads

```typescript theme={null}
// Example test payload for Stripe
{
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_test_123",
      "amount": 2000,
      "currency": "usd",
      "metadata": {
        "orderId": "order_456"
      }
    }
  }
}
```

## Security Best Practices

<AccordionGroup>
  <Accordion title="✅ Use Environment Variables">
    Never hardcode secrets

    ```typescript theme={null}
    // ❌ Bad
    secret: 'hardcoded_secret_key'

    // ✅ Good
    secret: env('STRIPE_WEBHOOK_SECRET')
    ```
  </Accordion>

  <Accordion title="✅ Validate Webhook Structure">
    Always validate incoming data

    ```typescript theme={null}
    execute: async (event) => {
      const { body } = event;
      if (!body || !body.type) {
        throw new Error('Invalid webhook payload');
      }
      // Process webhook...
    }
    ```
  </Accordion>

  <Accordion title="✅ Return Quickly">
    Webhook handlers should return fast (\< 5 seconds)

    ```typescript theme={null}
    // ✅ Queue long-running work
    execute: async (event) => {
      const { body } = event;

      // Quick validation
      if (!isValid(body)) {
        return { error: 'Invalid' };
      }
      
      // Queue processing job
      await Jobs.create({
        execute: async () => {
          // Long-running work here
        }
      });
      
      return { received: true };
    }
    ```
  </Accordion>
</AccordionGroup>

## Error Handling

```typescript theme={null}
const robustWebhook = new LuaWebhook({
  name: 'robust-webhook',
  
  execute: async (event) => {
    try {
      const result = await processWebhook(event);
      
      return {
        success: true,
        result,
        timestamp: new Date().toISOString()
      };
      
    } catch (error) {
      // Log error for debugging
      console.error('Webhook error:', {
        error: error.message,
        stack: error.stack
      });
      
      // Return error status (don't throw!)
      return {
        success: false,
        error: error.message,
        timestamp: new Date().toISOString()
      };
    }
  }
});
```

## Invoking an Agent from a Webhook

Use `Agents.invoke` to delegate work to a conversational agent after receiving a webhook event. Pass the user ID from the event payload so the invocation runs in that user's context (conversation history is stored). If no user ID is available, omit `userId` and the invocation runs without user identity (no conversation history).

```typescript theme={null}
import { LuaWebhook, Agents } from 'lua-cli';

const orderShippedWebhook = new LuaWebhook({
  name: 'order-shipped-webhook',
  description: 'Delegate shipment notifications to the notification agent',

  execute: async (event) => {
    const { orderId, customerId, trackingNumber } = event.body ?? {};

    if (!customerId) {
      return { skipped: true, reason: 'no customerId in payload' };
    }

    const result = await Agents.invoke('notification-agent', {
      prompt: `Order ${orderId} has shipped. Tracking: ${trackingNumber}. Notify the customer.`,
      userId: customerId, // runs as this user — history is stored
    });

    return { notified: true, agentReply: result.text };
  },
});

export default orderShippedWebhook;
```

<Card title="Agents API Reference" href="/api/agents" icon="robot">
  Full documentation for Agents.invoke — options, output shape, error handling, and more examples
</Card>

## Common Integrations

<Tabs>
  <Tab title="Stripe">
    **Webhook Events:**

    * `payment_intent.succeeded`
    * `payment_intent.payment_failed`
    * `charge.refunded`
    * `invoice.payment_succeeded`

    **Secret Location:** Stripe Dashboard → Developers → Webhooks
  </Tab>

  <Tab title="Shopify">
    **Webhook Topics:**

    * `orders/create`
    * `orders/updated`
    * `products/create`
    * `products/delete`

    **Secret Location:** Shopify Admin → Settings → Notifications
  </Tab>

  <Tab title="GitHub">
    **Webhook Events:**

    * `push`
    * `pull_request`
    * `deployment_status`
    * `release`

    **Secret Location:** Repository → Settings → Webhooks
  </Tab>

  <Tab title="Custom">
    **Requirements:**

    * POST request to webhook URL
    * JSON payload in body
    * Optional signature verification
    * Event type in payload or header

    **Best Practices:**

    * Include timestamp
    * Version your payloads
    * Support replay/retry
  </Tab>
</Tabs>

## Related APIs

<CardGroup cols={2}>
  <Card title="LuaAgent" href="/api/luaagent" icon="robot">
    Agent configuration
  </Card>

  <Card title="LuaTrigger" href="/api/luatrigger" icon="bolt">
    Declarative triggers that wake the agent instead of running code
  </Card>

  <Card title="Agents API" href="/api/agents" icon="robot">
    Invoke another agent from a webhook
  </Card>

  <Card title="Jobs API" href="/api/jobs" icon="clock">
    Queue long-running work
  </Card>

  <Card title="User API" href="/api/user" icon="user">
    Send notifications
  </Card>

  <Card title="Data API" href="/api/data" icon="database">
    Store webhook data
  </Card>
</CardGroup>

## See Also

* [LuaAgent](/api/luaagent) - Adding webhooks to your agent
* [LuaTrigger](/api/luatrigger) - Wake the agent on an event, no execute function
* [Agents API](/api/agents) - Invoking agents from webhook handlers
* [Environment Variables](/api/environment) - Securely managing secrets
* [Workflows Concept](/concepts/workflows)
