$npx -y skills add hookdeck/webhook-skills --skill openclaw-webhooksReceive and verify OpenClaw Gateway webhooks. Use when handling webhook events from OpenClaw AI agents, processing agent hook calls, wake events, or building integrations that respond to OpenClaw agent activity.
| 1 | # OpenClaw Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - Receiving webhook calls from an OpenClaw Gateway |
| 6 | - Verifying `Authorization: Bearer <token>` or `x-openclaw-token` headers |
| 7 | - Handling `/hooks/agent` and `/hooks/wake` event payloads |
| 8 | - Building external services that react to OpenClaw agent activity |
| 9 | |
| 10 | ## Essential Code (USE THIS) |
| 11 | |
| 12 | ### OpenClaw Token Verification (JavaScript) |
| 13 | |
| 14 | ```javascript |
| 15 | const crypto = require('crypto'); |
| 16 | |
| 17 | function verifyOpenClawWebhook(authHeader, xTokenHeader, secret) { |
| 18 | // OpenClaw sends the token in one of two headers: |
| 19 | // Authorization: Bearer <token> |
| 20 | // x-openclaw-token: <token> |
| 21 | const token = extractToken(authHeader, xTokenHeader); |
| 22 | if (!token || !secret) return false; |
| 23 | |
| 24 | try { |
| 25 | return crypto.timingSafeEqual( |
| 26 | Buffer.from(token), |
| 27 | Buffer.from(secret) |
| 28 | ); |
| 29 | } catch { |
| 30 | return false; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | function extractToken(authHeader, xTokenHeader) { |
| 35 | if (xTokenHeader) return xTokenHeader; |
| 36 | if (authHeader && authHeader.startsWith('Bearer ')) |
| 37 | return authHeader.slice(7); |
| 38 | return null; |
| 39 | } |
| 40 | ``` |
| 41 | |
| 42 | ### Express Webhook Handler |
| 43 | |
| 44 | ```javascript |
| 45 | const express = require('express'); |
| 46 | const app = express(); |
| 47 | |
| 48 | app.post('/webhooks/openclaw', |
| 49 | express.json(), |
| 50 | (req, res) => { |
| 51 | const authHeader = req.headers['authorization']; |
| 52 | const xToken = req.headers['x-openclaw-token']; |
| 53 | |
| 54 | if (!verifyOpenClawWebhook(authHeader, xToken, process.env.OPENCLAW_HOOK_TOKEN)) { |
| 55 | console.error('OpenClaw token verification failed'); |
| 56 | return res.status(401).send('Invalid token'); |
| 57 | } |
| 58 | |
| 59 | const { message, name, wakeMode, agentId, sessionKey } = req.body; |
| 60 | |
| 61 | console.log(`[${name || 'OpenClaw'}] ${message}`); |
| 62 | |
| 63 | // Respond quickly - OpenClaw expects 200 or 202 |
| 64 | res.status(200).json({ received: true }); |
| 65 | } |
| 66 | ); |
| 67 | ``` |
| 68 | |
| 69 | ### Python Token Verification (FastAPI) |
| 70 | |
| 71 | ```python |
| 72 | import hmac |
| 73 | |
| 74 | def verify_openclaw_webhook(auth_header: str | None, x_token: str | None, secret: str) -> bool: |
| 75 | token = x_token |
| 76 | if not token and auth_header and auth_header.startswith("Bearer "): |
| 77 | token = auth_header[7:] |
| 78 | if not token or not secret: |
| 79 | return False |
| 80 | return hmac.compare_digest(token, secret) |
| 81 | ``` |
| 82 | |
| 83 | > **For complete working examples with tests**, see: |
| 84 | > - [examples/express/](examples/express/) - Full Express implementation |
| 85 | > - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation |
| 86 | > - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation |
| 87 | |
| 88 | ## Webhook Endpoints |
| 89 | |
| 90 | OpenClaw Gateway exposes two webhook endpoints. Your external service receives POSTs from the Gateway (or a relay like Hookdeck) on a URL you choose. |
| 91 | |
| 92 | | Endpoint | Purpose | Response | |
| 93 | |----------|---------|----------| |
| 94 | | `POST /hooks/agent` | Trigger an isolated agent turn | `202 Accepted` | |
| 95 | | `POST /hooks/wake` | Enqueue a system event | `200 OK` | |
| 96 | |
| 97 | ## Agent Hook Payload |
| 98 | |
| 99 | ```json |
| 100 | { |
| 101 | "message": "Summarize inbox", |
| 102 | "name": "Email", |
| 103 | "agentId": "hooks", |
| 104 | "sessionKey": "hook:email:msg-123", |
| 105 | "wakeMode": "now", |
| 106 | "deliver": true, |
| 107 | "channel": "last", |
| 108 | "to": "+15551234567", |
| 109 | "model": "openai/gpt-5.2-mini", |
| 110 | "thinking": "low", |
| 111 | "timeoutSeconds": 120 |
| 112 | } |
| 113 | ``` |
| 114 | |
| 115 | | Field | Required | Description | |
| 116 | |-------|----------|-------------| |
| 117 | | `message` | Yes | Prompt or message for the agent | |
| 118 | | `name` | No | Human-readable hook name (e.g. "GitHub", "Email") | |
| 119 | | `agentId` | No | Route to a specific agent; falls back to default | |
| 120 | | `sessionKey` | No | Session key (disabled by default) | |
| 121 | | `wakeMode` | No | `now` (default) or `next-heartbeat` | |
| 122 | | `deliver` | No | Send agent response to messaging channel (default `true`) | |
| 123 | | `channel` | No | `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `msteams` | |
| 124 | | `to` | No | Recipient identifier for the channel | |
| 125 | | `model` | No | Model override for this run | |
| 126 | | `thinking` | No | Thinking level: `low`, `medium`, `high` | |
| 127 | | `timeoutSeconds` | No | Max duration for the agent run | |
| 128 | |
| 129 | ## Wake Hook Payload |
| 130 | |
| 131 | ```json |
| 132 | { |
| 133 | "text": "New email received", |
| 134 | "mode": "now" |
| 135 | } |
| 136 | ``` |
| 137 | |
| 138 | | Field | Required | Description | |
| 139 | |-------|----------|-------------| |
| 140 | | `text` | Yes | Description of the event | |
| 141 | | `mode` | No | `now` (default) or `next-heartbeat` | |
| 142 | |
| 143 | ## Authentication Headers |
| 144 | |
| 145 | OpenClaw supports two header styles. Pick one: |
| 146 | |
| 147 | | Header | Format | |
| 148 | |--------|--------| |
| 149 | | `Authorization` | `Bearer <token>` (recommended) | |
| 150 | | `x-openclaw-token` | `<token>` | |
| 151 | |
| 152 | Query-string tokens (`?token=...`) are rejected with `400`. |
| 153 | |
| 154 | ## Response Codes |
| 155 | |
| 156 | | Code | Meaning | |
| 157 | |------|---------| |
| 158 | | `200` | Wake event accepted | |
| 159 | | `202` | Agent hook accepted (async run started) | |
| 160 | | `400` | Invalid payload or query-string token | |
| 161 | | `401 |