Skip to main content

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.

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.
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;
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.
New in v3.0.0: HTTP webhooks for external integrations. Use with LuaAgent.

Use Cases

Payment Events

Stripe, PayPal webhooks for payment processing

E-commerce Updates

Shopify, WooCommerce order notifications

Git Events

GitHub, GitLab deployment triggers

Custom Integrations

Any service that sends HTTP webhooks

User Access in Webhooks

ContextHow to Get UseruserId Required?
WebhooksUser.get(userId)YES - Store in metadata
ToolsUser.get()❌ No - automatic context
LuaJobUser.get(userId)YES - Store in metadata
Jobs APIjobInstance.user()❌ No - automatic context
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.

Constructor

new LuaWebhook(config)

Creates a new webhook endpoint.
config
LuaWebhookConfig
required
Webhook configuration object

Configuration Parameters

Required Fields

name
string
required
Unique webhook nameFormat: lowercase, hyphens, underscoresExamples: 'payment-webhook', 'order-update-webhook'
execute
function
required
Function that handles incoming webhook eventsSignature: (event: WebhookEvent) => Promise<any>
execute: async (event) => {
  const { query, headers, body } = event;
  // ...
}

Optional Fields

description
string
Webhook description for documentation

WebhookEvent Shape

Every webhook receives a single object with request details:
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

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

Shopify Order Webhook

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;
Customer Identification: Map external customer IDs (Shopify, Stripe) to Lua user IDs. Store this mapping in your order/payment metadata for webhook notifications.

GitHub Deployment Webhook

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

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.
Event subscriptions currently support WhatsApp status events. Additional channels may emit these events in the future.

Available Event Types

EventTriggerChannel
message.sentMessage was sent to the recipientWhatsApp
message.deliveredMessage was delivered to the recipient’s deviceWhatsApp
message.readRecipient read the messageWhatsApp
message.failedMessage failed to sendWhatsApp
message.playedRecipient played a voice/video messageWhatsApp

Subscribing via CLI

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

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

lua test
# Select: Webhook → your-webhook-name
# Provide test payload

Test Payloads

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

Never hardcode secrets
// ❌ Bad
secret: 'hardcoded_secret_key'

// ✅ Good
secret: env('STRIPE_WEBHOOK_SECRET')
Always validate incoming data
execute: async (event) => {
  const { body } = event;
  if (!body || !body.type) {
    throw new Error('Invalid webhook payload');
  }
  // Process webhook...
}
Webhook handlers should return fast (< 5 seconds)
// ✅ 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 };
}

Error Handling

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).
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;

Agents API Reference

Full documentation for Agents.invoke — options, output shape, error handling, and more examples

Common Integrations

Webhook Events:
  • payment_intent.succeeded
  • payment_intent.payment_failed
  • charge.refunded
  • invoice.payment_succeeded
Secret Location: Stripe Dashboard → Developers → Webhooks

LuaAgent

Agent configuration

Agents API

Invoke another agent from a webhook

Jobs API

Queue long-running work

User API

Send notifications

Data API

Store webhook data

See Also