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.Event happens in external tool (user submits form)
- 2.Tool sends webhook to https://your-domain.com/webhooks/formtool
- 3.OpenClaw receives it, validates signature
- 4.OpenClaw processes the data (stores it, triggers skill, sends notification)
- 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.User asks OpenClaw: “Mark task #123 as done”
- 2.Skill prepares webhook payload
- 3.Skill sends POST to https://external-tool.com/webhooks/tasks
- 4.External tool receives it, validates, processes
- 5.External tool responds with HTTP 200 (or error)
- 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: 1000Restart OpenClaw with pm2 restart openclaw. Your endpoint is now live.
Step 3: Configure External Tool
- 1.Find “Webhooks” or “Integrations” settings
- 2.Add webhook URL: https://your-domain.com/webhooks/generic
- 3.If tool asks for “secret” or “signing key”, use your secret
- 4.Enable the webhook
- 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.External tool has a shared secret (you set this when configuring the webhook)
- 2.Tool generates a signature: HMAC_SHA256(payload, secret)
- 3.Tool sends signature in a header: X-Webhook-Signature: abc123…
- 4.You receive the webhook, compute the same signature using your copy of the secret
- 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)
);
}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: exponentialStep 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.

