A webhook can be genuine and still run twice. Providers retry after timeouts, operators redeliver missed events, and a captured signed request can be sent again while its freshness window is open. If every receipt wakes an agent, delivery mechanics become agent actions.
OpenClaw's generic HTTP hooks provide a Gateway ingress check. The current Inbound webhooks documentation accepts a dedicated token in the Authorization or x-openclaw-token header. That token authenticates the call into OpenClaw. It does not authenticate a provider's original sender, prove freshness, or create a durable exactly-once record. Provider signature checking and replay policy belong in front of the Gateway.
The order is: preserve raw bytes, verify the sender's signature, enforce freshness, claim a stable event key in durable storage, then submit one fixed agent request with the same idempotency key on retries.
Evidence boundary. OpenClaw settings and runtime behavior below are documented facts. The adapter, schema, and test matrix are proposed guidance. No live Gateway or provider endpoint was tested for this article.
What the receiver must prove
| Check | Question | Limit |
|---|---|---|
| Authenticity | Did the provider create these exact bytes? | A signature does not make an old request new. |
| Freshness | Is the signed request inside the provider's time or nonce window? | Only providers that sign and document freshness support this check. |
| Uniqueness | Has this logical event already been claimed? | Use durable storage and an atomic uniqueness constraint. |
| Gateway admission | Has the event reached the intended OpenClaw run? | OpenClaw's replay key is useful, but its cache is not a receipt ledger. |
| Agent authority | What may the agent do with authenticated content? | A valid sender can still send hostile instructions or sensitive data. |
Start with the sender's contract
Record the upstream service's actual signature header, algorithm, signed bytes or canonical string, digest encoding, timestamp or nonce rules, stable retry identifier, event types, body limit, and response-time limit. Do not invent a universal X-Webhook-Signature format.
- Choose whether the database key represents a logical event or one physical delivery.
- Keep the provider secret separate from the OpenClaw hook token.
- Use the provider's documented event and action allowlist.
GitHub exposes X-GitHub-Delivery as a unique delivery identifier and keeps it unchanged when that delivery is redelivered, according to its webhook best practices. Stripe recommends recorded event IDs for repeated receipts and, in some cases, object ID plus event type for separate Event objects representing the same logical change in its webhook guidance. Those examples are provider-specific, not an OpenClaw rule.
Verify the raw request before parsing it
Signature verification must see the bytes the provider signed. GitHub says the payload and headers must not be modified before verification and recommends HMAC-SHA256 with a constant-time comparison. Stripe's Node example keeps the body unparsed until its library checks the signature.
For an HMAC provider, the control flow looks like this. The header parser and signed message format remain provider-specific:
const rawBody = await readRawBody(request);
const signatureHeader = request.headers['x-provider-signature'];
const received = parseProviderSignature(signatureHeader);
const expected = createHmac('sha256', providerSecret)
.update(rawBody)
.digest();
if (!received ||
received.length !== expected.length ||
!timingSafeEqual(received, expected)) {
return reject(401);
}
const event = JSON.parse(rawBody.toString('utf8'));
Node's crypto documentation defines createHmac and says timingSafeEqual compares bytes with a constant-time algorithm. The values must have equal length before that call; the function does not make the surrounding handler automatically timing-safe.
Read the body with a size cap and timeout before expensive work. OpenClaw documents a normal generic-hook limit of 256 KiB and a 30-second body-read timeout. An adapter can choose a smaller limit for a narrower event.
Freshness is separate from signature validity
HMAC is a shared-key message authentication code. RFC 2104 describes shared-key authentication and recommends random keys with periodic refresh. It does not provide a replay window: a valid MAC over an old message is still a valid MAC.
If the provider signs a timestamp, verify it before dispatch. Stripe signs the timestamp as part of its signed value, recommends rejecting old timestamps, and documents a default five-minute tolerance in its libraries. That value belongs to Stripe; use the contract for the provider you are integrating. Keep the receiver clock synchronized, and do not use a zero tolerance where the provider says that disables the recency check.
A retry can receive a new signature and timestamp for the same event, so the signature string is not an idempotency key. If there is no signed timestamp, nonce, or equivalent freshness value, durable event identity still blocks repeated processing after the first claim but cannot prove that an unseen signed request was not captured long ago.
Claim the event atomically
The receiver needs a durable claim before it wakes OpenClaw. A process-local set disappears on restart, and a SELECT followed by an INSERT can let concurrent deliveries pass together.
This is proposed schema, adapted to the idempotency pattern in Stripe's migration guide:
CREATE TABLE webhook_receipts (
provider TEXT NOT NULL,
endpoint TEXT NOT NULL,
event_key TEXT NOT NULL,
body_hash TEXT NOT NULL,
state TEXT NOT NULL,
received_at TEXT NOT NULL,
PRIMARY KEY (provider, endpoint, event_key)
);
Choose event_key from the sender's contract and scope it by provider, endpoint, and account or tenant when needed. Hash the verified raw body so a repeated key with different bytes can be detected. Do not silently replace the first receipt.
On a uniqueness conflict, the same key and same hash is a duplicate: acknowledge it and skip dispatch. The same key with different bytes belongs in quarantine or manual review. Store an outbox job with the receipt, or use an equivalent recoverable handoff, so a crash after the claim does not lose the event. Store the raw body only when audit or retry requires it, with a retention and access policy.
Use OpenClaw idempotency as a second fence
Configure the Gateway with a dedicated hook token, a fixed agent allowlist, and caller-selected sessions disabled unless they are needed:
{
hooks: {
enabled: true,
token: '<dedicated-hook-token>',
path: '/hooks',
allowedAgentIds: ['webhook-reader'],
allowRequestSessionKey: false
}
}
The OpenClaw hook configuration reference says the hook token must be distinct from Gateway authentication. It accepts header authentication and rejects query-string tokens. It also documents agent allowlists and session-key prefix restrictions.
Have the worker call /hooks/agent with a fixed agentId, isolated session mode, a narrow message, and an Idempotency-Key derived from the durable event key. Reuse that key, message, and routing for the same job. Do not pass through provider-controlled agent, session, channel, recipient, or model fields.
OpenClaw resolves replay keys from Idempotency-Key, X-OpenClaw-Idempotency-Key, then payload idempotencyKey. Keys are limited to 256 characters and replay only when token, path, and resolved dispatch fields match. Changing message or routing can create a new run. Terminal entries expire after five minutes within a 1,000-entry memory bound, and a Gateway restart clears replay state. The docs call this not durable exactly-once delivery, so the database receipt stays the first fence.
Acknowledge durable admission, not model completion
- Read the bounded raw body and provider headers.
- Verify the signature against those bytes.
- Check freshness, parse the event, and allow only needed types and fields.
- Atomically insert the receipt and an outbox job.
- Return the provider's success response after that handoff.
- Let a worker call OpenClaw with the same key and record the run ID.
GitHub recommends a 2XX response within 10 seconds and asynchronous processing. Stripe also says to respond before complex work that may time out. If storage is unavailable, let the provider retry. For an invalid signature, stale new event, malformed body, or unsupported event, follow the provider's retry contract but never call OpenClaw.
OpenClaw's 200 response means runner admission, not model completion, tool success, or message delivery. A lost response can be replayed with the same key, but completion monitoring and business-side effects need their own records.
Failure paths to decide before launch
| Condition | Receiver action | Limit |
|---|---|---|
| Bad signature or stale new request | Reject without an OpenClaw call and log a reason code. | Retry behavior is provider-specific. |
| Known key, same body, running or completed | Acknowledge and skip dispatch. | Retain trusted state for redelivery. |
| Known key, different body | Quarantine and alert. | Do not overwrite or reuse the run. |
| OpenClaw returns 400 or 401 | Mark failed and fix routing or credentials. | Blind retries repeat configuration errors. |
| OpenClaw returns 429, 502, 503, or times out | Retry the job with the same key and honor Retry-After where present. | Admission and completion remain separate. |
| Crash or Gateway restart | Reconcile pending receipts from durable storage. | Do not rely on the process-local replay cache. |
Contain the agent after the checks
Authentication does not make event content safe. OpenClaw describes hook tokens as ingress access rather than authenticated sender identity. Its webhook documentation says agent-hook content is safety-wrapped by default, but that wrapping does not remove tools or workspace access.
Use a separate restricted agent for external events. The tool and agent permissions guidance gives examples that deny gateway, cron, sessions_spawn, and sessions_send for untrusted content, and documents no-workspace or read-only profiles. Keep the Gateway behind loopback, a private network, or a trusted reverse proxy. Use /hooks/agent for untrusted event text; a raw /hooks/wake message is a system event, not a restricted reader.
Proposed verification plan
Proposed tests, not executed here. Use a disposable provider endpoint and test agent:
- One valid event produces one receipt, one outbox job, and one admission.
- Two concurrent copies produce one agent run.
- One changed body byte with the old signature produces no OpenClaw call.
- A valid stale signature produces no new dispatch.
- The same key with different bytes is quarantined.
- A worker crash after the claim recovers with the same key.
- A Gateway restart does not bypass the durable receipt.
Track accepted, rejected, duplicate, conflict, queued, failed, and completed states. Keep provider IDs and OpenClaw run IDs for correlation, but redact tokens, signatures, private fields, and raw model context. The target is not a promise of exactly-once execution across every component. It is a receiver whose duplicate side effects are unlikely, visible, and recoverable.
Sources
- Inbound webhooks - OpenClaw: hook authentication, body limits, idempotency, admission responses, network placement, and agent-content boundaries. Accessed 2026-09-13.
- Configuration - hooks - OpenClaw: dedicated tokens, header-only authentication, agent and session policy, replay-key scope, cache lifetime, and restart behavior. Accessed 2026-09-13.
- Tool and agent permissions - OpenClaw: restricted tools, workspace profiles, sandbox access, and untrusted-content controls. Accessed 2026-09-13.
- Validating webhook deliveries - GitHub Docs: raw-payload HMAC-SHA256 validation, UTF-8 handling, and constant-time comparison. Accessed 2026-09-13.
- Best practices for using webhooks - GitHub Docs: delivery identifiers, replay defense, asynchronous processing, and response timing. Accessed 2026-09-13.
- Receive Stripe events in your webhook endpoint - Stripe Documentation: raw-body verification, duplicate handling, signed timestamps, retry signatures, and quick acknowledgement. Accessed 2026-09-13.
- Migrate from snapshot events to thin events - Stripe Documentation: an idempotency table with a primary key, atomic duplicate detection, and logical event correlation. Accessed 2026-09-13.
- Crypto - Node.js v26.8.2 Documentation:
createHmacand the length and constant-time behavior oftimingSafeEqual. Accessed 2026-09-13. - RFC 2104: HMAC: Keyed-Hashing for Message Authentication: shared-key message authentication and random, periodically refreshed keys. Published 1997-02; accessed 2026-09-13.
Reference Trail
Sources and further reading
- Inbound webhooks documentationdocs.openclaw.ai
- webhook best practicesdocs.github.com
- webhook guidancedocs.stripe.com
- crypto documentationnodejs.org
- RFC 2104www.rfc-editor.org