Skip to main content

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;

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

Common Integrations

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

See Also