On this page

How to Integrate OpenClaw with Any Tool via Webhooks

Not every tool has an official OpenClaw skill. But almost every modern SaaS tool has webhooks.

Webhooks are how tools talk to each other when there’s no pre-built integration. They’re HTTP requests with JSON payloads that say “something happened” or “please do this thing.”

This guide shows you how to connect OpenClaw to anything via webhooks—whether you’re receiving data from external tools (inbound webhooks) or sending commands to them (outbound webhooks).

By the end, you’ll be able to integrate OpenClaw with Airtable, Notion, Stripe, custom internal tools, or that legacy system your company built in 2015 that somehow still runs everything.

What Are Webhooks (Actually)

A webhook is an HTTP POST request sent when an event happens.

Example: Stripe sends a webhook when a payment succeeds:

POST https://your-openclaw-domain.com/webhooks/stripe

{
  "event": "payment.succeeded",
  "amount": 9900,
  "currency": "usd",
  "customer": "cus_123",
  "timestamp": "2026-05-06T10:30:00Z"
}

Your OpenClaw instance receives this, processes it, and can:

  • Send a Slack message: “Payment received: $99”
  • Update a Google Sheet with payment data
  • Trigger a skill to send a receipt email

That’s an inbound webhook (external tool → OpenClaw).

Outbound webhooks go the other direction. OpenClaw skill wants to update a task in your project management tool:

POST https://your-pm-tool.com/api/webhooks/tasks

{
  "action": "update",
  "task_id": "task_456",
  "status": "completed",
  "completed_by": "openclaw_agent"
}

OpenClaw → external tool.

Why Webhooks Instead of APIs?

APIs require active polling: “Are there new items? How about now? Now?”

Webhooks are push-based: “New item just appeared. Here it is.”

When to Use Each

Use regular API calls when:

  • You need to pull data on demand (“show me my tasks”)
  • You’re okay with some delay (polling every minute)
  • The tool doesn’t support webhooks

Use webhooks when:

  • You need real-time notifications (payment succeeded, form submitted)
  • You want to trigger OpenClaw actions based on external events
  • You’re integrating with tools that specifically offer webhooks

Most modern tools support both. Webhooks are just more efficient for event-driven workflows.

Architecture: Inbound vs Outbound Webhooks

Inbound Webhooks (Tool → OpenClaw)

External tool sends HTTP POST to your OpenClaw webhook endpoint.

  1. 1.Event happens in external tool (user submits form)
  2. 2.Tool sends webhook to https://your-domain.com/webhooks/formtool
  3. 3.OpenClaw receives it, validates signature
  4. 4.OpenClaw processes the data (stores it, triggers skill, sends notification)
  5. 5.OpenClaw responds with HTTP 200 (success) or 500 (error)

Requirements:

  • Public OpenClaw endpoint (not localhost)
  • HTTPS (most tools require SSL)
  • Webhook signature validation (security)

Outbound Webhooks (OpenClaw → Tool)

OpenClaw skill sends HTTP POST to external tool’s webhook endpoint.

  1. 1.User asks OpenClaw: “Mark task #123 as done”
  2. 2.Skill prepares webhook payload
  3. 3.Skill sends POST to https://external-tool.com/webhooks/tasks
  4. 4.External tool receives it, validates, processes
  5. 5.External tool responds with HTTP 200 (or error)
  6. 6.Skill confirms to user: “Task marked done”

Setting Up Inbound Webhooks

Step 1: Create a Webhook Endpoint in OpenClaw

OpenClaw needs a route to receive webhooks. Create webhooks/generic.js:

export default async function handleWebhook(req, res) {
  const { headers, body } = req;

  console.log('Webhook received:', { headers, body, timestamp: new Date().toISOString() });

  const isValid = validateSignature(headers, body);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  try {
    await processWebhookData(body);
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
}

Step 2: Register the Endpoint

In config/webhooks.yml:

webhooks:
  inbound:
    - name: generic
      path: /webhooks/generic
      handler: webhooks/generic.js
      enabled: true
      signature_validation: true
      signature_header: X-Webhook-Signature
      secret: your-webhook-secret-here
      rate_limit:
        requests_per_minute: 60
        requests_per_hour: 1000

Restart OpenClaw with pm2 restart openclaw. Your endpoint is now live.

Step 3: Configure External Tool

  1. 1.Find “Webhooks” or “Integrations” settings
  2. 2.Add webhook URL: https://your-domain.com/webhooks/generic
  3. 3.If tool asks for “secret” or “signing key”, use your secret
  4. 4.Enable the webhook
  5. 5.Test it (most tools have a “Test webhook” button)

Step 4: Verify It Works

pm2 logs openclaw | grep webhook

Webhook Signature Validation (Critical for Security)

Problem: Anyone can send HTTP POST to your webhook endpoint. How do you know it’s actually from the legitimate tool and not an attacker?

Solution: Webhook signatures.

How It Works

  1. 1.External tool has a shared secret (you set this when configuring the webhook)
  2. 2.Tool generates a signature: HMAC_SHA256(payload, secret)
  3. 3.Tool sends signature in a header: X-Webhook-Signature: abc123…
  4. 4.You receive the webhook, compute the same signature using your copy of the secret
  5. 5.If your signature matches theirs: legitimate request. Otherwise reject it.

Implementation

import crypto from 'crypto';

function validateSignature(headers, body, secret) {
  const receivedSignature = headers['x-webhook-signature'];
  if (!receivedSignature) return false;

  const payload = JSON.stringify(body);
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(receivedSignature),
    Buffer.from(expectedSignature)
  );
}
? Note:Each tool uses slightly different signature schemes — Stripe, GitHub, Slack, and Twilio all have their own header names and computation rules. Always check the tool’s documentation.

Common Pitfalls

Payload order matters. Use the raw request body before parsing:

app.use('/webhooks', express.raw({ type: 'application/json' }));

const rawBody = req.body.toString('utf8');
const expectedSignature = crypto
  .createHmac('sha256', secret)
  .update(rawBody)
  .digest('hex');

Timestamp validation prevents replay attacks. Reject if older than ~5 minutes and include the timestamp in the signed payload.

Setting Up Outbound Webhooks

Step 1: Create an Outbound Webhook Skill

export default {
  name: 'webhook_sender',
  description: 'Send data to external tools via webhook',

  async execute({ url, method = 'POST', data, headers = {}, secret }) {
    const payload = JSON.stringify(data);

    if (secret) {
      const signature = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');
      headers['X-Webhook-Signature'] = signature;
    }

    const response = await fetch(url, {
      method,
      headers: { 'Content-Type': 'application/json', ...headers },
      body: payload
    });

    if (!response.ok) {
      throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
    }
    return await response.json();
  }
};

Step 2: Configure Webhook Destinations

webhooks:
  outbound:
    - name: project_management
      url: https://your-pm-tool.com/api/webhooks
      method: POST
      headers:
        Authorization: Bearer your-api-key-here
      secret: shared-secret-if-needed
      retry:
        enabled: true
        max_attempts: 3
        backoff: exponential

Step 3: Implement Retry Logic

Webhooks fail. Networks are unreliable. Handle it gracefully with exponential backoff (1s, 2s, 4s) and skip retries on 4xx client errors.

async function sendWebhookWithRetry(destination, data, maxAttempts = 3) {
  let attempt = 0;
  let delay = 1000;

  while (attempt < maxAttempts) {
    try {
      const response = await fetch(destination.url, {
        method: destination.method,
        headers: destination.headers,
        body: JSON.stringify(data)
      });
      if (response.ok) return await response.json();
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`);
      }
      throw new Error(`Server error: ${response.status}`);
    } catch (error) {
      attempt++;
      if (attempt >= maxAttempts) throw error;
      await new Promise(r => setTimeout(r, delay));
      delay *= 2;
    }
  }
}

Real-World Integration Examples

Example 1: Stripe Payment Webhooks (Inbound)

When payment succeeds, notify team on Slack.

export default async function stripeWebhook(req, res) {
  const sig = req.headers['stripe-signature'];
  const secret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, secret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payment_intent.succeeded') {
    const payment = event.data.object;
    await sendSlackMessage({
      channel: '#payments',
      text: `Payment received: $${payment.amount / 100} from ${payment.customer}`
    });
  }

  res.json({ received: true });
}

Example 2: Form Submission to Google Sheets

When form submitted, append a row to a Google Sheet and send a confirmation email — combining inbound and outbound flows.

Example 3: GitHub PR Opened → Create Task

export default async function githubWebhook(req, res) {
  const event = req.headers['x-github-event'];

  if (event === 'pull_request' && req.body.action === 'opened') {
    await sendWebhook('project_management', {
      action: 'create_task',
      title: `Review PR: ${req.body.pull_request.title}`,
      description: req.body.pull_request.html_url,
      assignee: 'code-review-team',
      due_date: addDays(new Date(), 2)
    });
  }

  res.status(200).send('OK');
}

Authentication Patterns

API Keys (Simplest)

headers: {
  'Authorization': 'Bearer your-api-key',
  'X-API-Key': 'your-api-key'
}

OAuth 2.0 (More Secure)

For tools like Google, Slack, Microsoft. Get an OAuth token via the standard flow, send it as a Bearer token, and refresh when you receive a 401.

HMAC Signatures (Tool-Specific)

Slack uses v0:timestamp:body as the signed string with the x-slack-signature header. GitHub uses sha256=... in the x-hub-signature-256 header. Always use timingSafeEqual for comparisons.

Webhook Debugging Cheatsheet

Problem: Webhook not received

  • Is endpoint public? Test with curl
  • Is HTTPS working? Most tools require SSL
  • Is firewall blocking? Check sudo ufw status
  • Is OpenClaw running? pm2 status openclaw
pm2 logs openclaw | grep webhook

curl -X POST https://your-domain.com/webhooks/generic 
  -H "Content-Type: application/json" 
  -d '{"test": true}'

Problem: Signature validation fails

  • Is secret correct? Check .env or config
  • Is payload being modified? Use raw body
  • Is signature header name correct?

Problem: Outbound webhook times out

const response = await fetch(url, {
  method: 'POST',
  body: JSON.stringify(data),
  timeout: 30000 // 30 seconds
});

Problem: Webhooks work sometimes but not always

  • Rate limiting: Sending too many webhooks too fast
  • Retry logic missing: Network issues cause silent failures
  • No queue: Sync processing blocks new webhooks

Monitoring Webhook Health

Track webhook_received_total, webhook_processing_duration, webhook_errors_total, and outbound_webhook_success_rate. Alert when error rate > 10% for 5 minutes or p95 latency > 5s for 10 minutes.

// GET /webhooks/health
export async function webhookHealth(req, res) {
  const checks = {
    inbound_endpoints: await checkInboundEndpoints(),
    outbound_destinations: await checkOutboundDestinations(),
    queue_depth: await getQueueDepth(),
    error_rate: await getErrorRate()
  };
  const healthy = Object.values(checks).every(c => c.status === 'ok');
  res.status(healthy ? 200 : 503).json(checks);
}

The Honest Cost of Webhook Integrations

DIY webhook setup: 1–2 hours setup per integration, 30min–2 hours debugging (signature validation is finicky), ~30min/month maintenance. At $50/hr that’s $100–200 setup + $25/month.

PaioClaw alternative: Built-in templates, automatic signature validation, default retry logic, monitoring dashboard. Total: $4/month.

DIY makes sense for custom internal tools, very specific requirements, or learning. PaioClaw makes sense for common SaaS tools when you need it working today.

The Bottom Line

Webhooks are the universal integration language. If a tool supports webhooks, you can connect OpenClaw to it—even if there’s no official skill.

Inbound webhooks let OpenClaw react to external events. Outbound webhooks let OpenClaw trigger actions in other tools. Together, they turn OpenClaw into the hub of your workflow.

The technical implementation is straightforward: HTTP POST + signature validation + retry logic. The hard part is debugging when signatures don’t match or payloads arrive in unexpected formats.

? Tip:If you’re integrating with 1–2 custom tools, DIY webhooks are manageable. If you’re connecting to Stripe, GitHub, Slack, Notion, and five other SaaS products, PaioClaw’s pre-built webhook templates save you dozens of hours.

Join Our Community

Connect with other PaioClaw users, share tips, and stay up to date.