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

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

## 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;
}
```

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

## 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** — currently, message delivery status updates from WhatsApp. When subscribed, your webhook's `execute` function receives the event payload automatically.

<Note>
  Event subscriptions currently support WhatsApp status events. Additional channels may emit these events in the future.
</Note>

### Available Event Types

| Event               | Trigger                                         | Channel  |
| ------------------- | ----------------------------------------------- | -------- |
| `message.sent`      | Message was sent to the recipient               | WhatsApp |
| `message.delivered` | Message was delivered to the recipient's device | WhatsApp |
| `message.read`      | Recipient read the message                      | WhatsApp |
| `message.failed`    | Message failed to send                          | WhatsApp |
| `message.played`    | Recipient played a voice/video message          | WhatsApp |

### 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` contains:

```typescript theme={null}
interface MessageStatusEvent {
  eventType: string;
  agentId: string;
  messageWamid: string;
  recipientId: string;
  status: 'sent' | 'delivered' | 'read' | 'failed' | 'played';
  channel: 'whatsapp';
  phoneNumberId: string;
  timestamp: string;
  conversation?: {
    id: string;
    origin: { type: string };
    expiration_timestamp?: string;
  };
  pricing?: {
    billable: boolean;
    pricing_model: string;
    category: string;
  };
  errors?: Array<{
    code: number;
    title: string;
    message?: string;
  }>;
}
```

### 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="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
* [Agents API](/api/agents) - Invoking agents from webhook handlers
* [Environment Variables](/api/environment) - Securely managing secrets
* [Workflows Concept](/concepts/workflows)
