# Guide to OpenClaw Webhooks: Features and Best Practices

OpenClaw is a self-hosted gateway that runs AI agents and connects them to chat apps, tools and schedules. Its automations, also called cron jobs, run an agent prompt, a command or a script on a schedule. Usually the result is announced in a chat, but an automation can instead POST its result to a URL. That makes an OpenClaw agent a source of events for your own systems: a daily digest written to a database, a scheduled check that opens a ticket, or a failed run that pages someone.

This guide covers the webhooks OpenClaw sends when automations finish or fail: how to set them up, how to authenticate them, what they contain, and how OpenClaw records delivery. OpenClaw also receives webhooks on its own `/hooks` endpoints. That's the other direction, and our tutorial on [using Hookdeck with OpenClaw](/webhooks/platforms/using-hookdeck-with-openclaw-reliable-webhooks-for-your-ai-agent) covers it.

## What are OpenClaw webhooks?

Every OpenClaw automation has a delivery mode that decides what happens with its output:

| Mode | What happens |
| --- | --- |
| `announce` | Sends the result to a chat and records confirmed delivery |
| `webhook` | POSTs the finished event payload to a URL |
| `none` | No automatic result or notification |

In `webhook` mode, OpenClaw sends a `POST` with a JSON body to your URL when a run finishes. Separately, failure alerts for an automation can also be sent to a webhook.

OpenClaw doesn't sign these requests. If you set `cron.webhookToken` in the Gateway's configuration, every automation webhook POST carries `Authorization: Bearer <token>`. That header is the only way to check that a request came from your OpenClaw Gateway.

## OpenClaw webhook features

| Feature | Details |
| --- | --- |
| Configuration | `--webhook <url>` on `openclaw automations add`, `create` or `edit` |
| Method and body | `POST` with a JSON finished-run payload |
| Authentication | `Authorization: Bearer <cron.webhookToken>` |
| Signature | None |
| Token scope | One token in the Gateway config, used for every automation webhook |
| Credentials in URL | Not allowed |
| Outbound policy | Strict SSRF guard. Private and loopback targets need an exemption |
| Empty results | A successful run with an empty summary sends nothing |
| Errors | Always sent, even without a summary |
| Success | Any `2xx` records the delivery |
| Lost responses | Recorded as **Unknown** and not retried |
| Failure alerts | Optional webhook destination with its own threshold and cooldown |
| Payload schema | Not in the docs. Fields below come from OpenClaw's source code |

## What OpenClaw sends

OpenClaw's documentation says only that webhook mode will "POST finished event payload to a URL". It doesn't list the payload's fields. The fields below come from OpenClaw's source code, so they may change between releases. Read them defensively.

A finished-run payload looks like this:

```json
{
  "jobId": "job_123",
  "action": "finished",
  "runId": "run_abc",
  "runAtMs": 1767268800000,
  "durationMs": 4200,
  "status": "ok",
  "completionStatus": "succeeded",
  "summary": "Three deploys today, all green.",
  "job": { "id": "job_123", "name": "Deploy digest" },
  "nextRunAtMs": 1767355200000
}

```

| Field | Description |
| --- | --- |
| `action` | `finished` for this delivery |
| `jobId`, `job` | The automation's ID and a snapshot of its definition |
| `runId`, `runAtMs`, `durationMs` | The run's ID, start time in milliseconds, and duration |
| `status` | Execution status, such as `ok` or `error` |
| `completionStatus` | Whether the run's required delivery completed, such as `succeeded` or `failed` |
| `summary` | The run's output. Left out on error events |
| `error` | Error text for failed runs |
| `nextRunAtMs` | The next scheduled run, when there is one |

`status` and `completionStatus` are separate. The docs say a run can record `status: "ok"` with `completionStatus: "failed"` when execution succeeded but required delivery didn't.

Failure alerts sent to a webhook have a smaller body in the source: `jobId`, `jobName`, `message` and `runAtMs`. OpenClaw's docs confirm the structured `runAtMs` field, and say failure webhooks keep the raw error for diagnostic integrations.

## Setting up OpenClaw webhooks

1. Choose a long random token and set it in the Gateway configuration:

```jsonc
{
  cron: {
    webhookToken: "replace-with-dedicated-webhook-token",
  },
}

```

1. Create an automation that delivers to your URL:

```bash
openclaw automations create "0 18 * * 1-5" \
  "Summarize today's deploys as JSON." \
  --name "Deploy digest" \
  --webhook "https://example.com/webhooks/openclaw"

```

Use `openclaw automations edit <job-id> --webhook <url>` to switch an existing job. Webhook delivery can't be combined with the chat delivery flags `--announce`, `--channel`, `--to`, `--thread-id` or `--account`.

1. Optionally, send failure alerts to a webhook too, through the job's `delivery.failureDestination` in webhook mode. By default, a job with a failure route alerts after 2 consecutive failures, with a 1-hour cooldown, and repeated failures with the same cause count as one incident. `openclaw automations edit` has flags to tune the threshold, cooldown and mode per job.

Don't put credentials in the URL. OpenClaw's docs say webhook URLs must not include an embedded username and password, and to use `webhookToken` instead.

### The SSRF guard

Every outbound automation webhook goes through a strict SSRF guard. Loopback, private, internal, link-local and other special-use addresses are refused by default, which covers primary delivery, completion and failure destinations, and failure-alert webhooks. A public HTTPS URL, such as a Hookdeck source URL, works without changes. To reach a receiver on your own network, add its exact hostname or IP:

```jsonc
{
  cron: {
    webhookSsrfPolicy: {
      allowedHostnames: ["127.0.0.1"],
    },
  },
}

```

`dangerouslyAllowPrivateNetwork: true` opens every private address to every automation webhook. OpenClaw recommends narrow `allowedHostnames` entries instead.

## Securing OpenClaw webhooks

Compare the whole `Authorization` header with `Bearer ` followed by your token, in constant time. There's no body signature to check, so this is the entire verification.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyOpenClawWebhook(authorizationHeader, webhookToken) {
  if (!authorizationHeader || !webhookToken) return false;
  const received = Buffer.from(authorizationHeader, 'utf8');
  const expected = Buffer.from(`Bearer ${webhookToken}`, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/openclaw', express.raw({ type: '*/*' }), (req, res) => {
  const webhook_token = process.env.OPENCLAW_WEBHOOK_TOKEN;
  if (!webhook_token) return res.sendStatus(500); // fail closed

  if (!verifyOpenClawWebhook(req.get('authorization'), webhook_token)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  if (event.action === 'finished') {
    // event.jobId, event.runId, event.status, event.summary or event.error
  } else if (event.jobName && event.message) {
    // A failure alert
  }
  res.sendStatus(200); // any 2xx counts as delivered
});

```

The same check in Python:

```python
import hmac

def verify_openclaw_webhook(authorization_header, webhook_token: str) -> bool:
    if not authorization_header or not webhook_token:
        return False
    expected = f"Bearer {webhook_token}".encode("utf-8")
    return hmac.compare_digest(authorization_header.encode("utf-8"), expected)

# In FastAPI:
# verify_openclaw_webhook(request.headers.get("authorization"),
#                         os.environ["OPENCLAW_WEBHOOK_TOKEN"])

```

OpenClaw only sends the header when `cron.webhookToken` is set, so always set it. Without it, anyone who finds your URL can post fake automation results.

In Hookdeck, the OpenClaw source type checks the `Authorization` header. Enter the whole value, `Bearer ` followed by your `webhookToken`.

> **Make OpenClaw webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the bearer token, stores every automation result as soon as OpenClaw sends it, and retries delivery to your service, which OpenClaw itself won't do.

## OpenClaw webhook limitations and pain points

### No retries for uncertain deliveries

**The Problem:** An automation result is lost because your receiver was slow or restarting.

**Why It Happens:** OpenClaw records a delivery after a `2xx`, and records **Not delivered** after an HTTP rejection. If the request may have reached the receiver but the response is lost or times out, delivery stays **Unknown**, and the transport doesn't retry that send. The docs describe the same behaviour for failure-alert webhooks.

**Workarounds:**

* Respond with `2xx` as soon as the request is authenticated and stored.
* Check automation run history for **Not delivered** and **Unknown** results.
* Run the job again manually if a result matters and didn't arrive.

**How Hookdeck Can Help:** Hookdeck acknowledges each request at its edge, so OpenClaw records a delivery, and then holds the event until your service accepts it. Failed deliveries to your service are [retried](/docs/retries) and can be replayed.

### One shared token, no signature

**The Problem:** Every automation on a Gateway uses the same secret, and a leaked token lets anyone send results for any job.

**Why It Happens:** `cron.webhookToken` is a single Gateway setting sent as a bearer header. Nothing ties a request to a particular job or protects the body.

**Workarounds:**

* Use a dedicated token for automation webhooks, separate from the Gateway's other tokens and from `hooks.token`.
* Give each automation its own URL path, and check that `jobId` matches the path you expect.
* Use HTTPS and keep the token out of logs.

**How Hookdeck Can Help:** Hookdeck checks the token at the [source](/docs/sources) and records any request that fails, so you can see when something other than your Gateway is calling the URL.

### Silent successes

**The Problem:** A scheduled job ran, but your system never heard about it.

**Why It Happens:** A successful run with an empty summary deliberately skips the POST and records `deliverySuppressionReason: "empty"`. Errors are still sent.

**Workarounds:**

* Make the automation's prompt or script always produce output, even "nothing to report".
* Use `nextRunAtMs` from earlier deliveries to notice a run that should have reported and didn't.

**How Hookdeck Can Help:** Hookdeck's event log shows exactly when each result arrived, which makes a missing run easy to spot.

### Undocumented payload

**The Problem:** A field your handler depends on is renamed or removed after an OpenClaw upgrade.

**Why It Happens:** The docs don't define the webhook payload. The field names come from the source code and aren't a published contract.

**Workarounds:**

* Read fields defensively, with defaults, and log payloads that don't match what you expect.
* Pin your OpenClaw version and test webhooks after each upgrade.

**How Hookdeck Can Help:** Every request is stored in Hookdeck, so after an upgrade you can compare old and new payloads and replay them once your handler is fixed. [Transformations](/docs/transformations) can map a changed payload back to the shape your service expects.

## Best practices

### Always set `webhookToken`

Without it, OpenClaw sends no `Authorization` header and your endpoint can't tell a real result from a fake one.

### Compare the whole header in constant time

Compare `Authorization` with `Bearer <token>` using `crypto.timingSafeEqual` or `hmac.compare_digest`, check lengths first in Node, and fail closed if the token isn't configured.

### Deduplicate on `runId`

Handle each run once. Use `runId`, or `jobId` with `runAtMs` for failure alerts, which have no `runId`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Handle results and failures separately

Check `action` and `status` before using `summary`, which is left out on errors, and treat failure alerts, which have `jobName` and `message`, as their own event type.

### Acknowledge fast and process asynchronously

Store the result, return `200`, and process afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

OpenClaw automations can send their results to any public URL with a single `--webhook` flag. Authentication is a bearer token you configure once per Gateway, the payload is defined only by the source code, and a delivery whose response is lost isn't retried. Setting the token, verifying it carefully and putting a durable receiver in front of your service covers most of the risk.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the token, stores every result, and delivers it to your service with retries you control.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle OpenClaw webhooks reliably in minutes.