Webhooks
A webhook subscription tells CheckFlow to POST a JSON document to a URL of yours whenever a particular event happens in the workspace — a checklist is started, a task is completed, a file is uploaded, a Data Set record changes. The nine events, what triggers each one and the exact body each one sends are on Webhook Events. This page covers the subscriptions themselves, what a delivery looks like when it arrives, how to prove it came from CheckFlow, and what happens when your endpoint does not accept it.
Subscriptions are not shown anywhere in the app. The API — these routes, the MCP tools that sit on them and the older v2 webhook routes — is the only way to create, list or remove one, so a subscription somebody set up and forgot is still delivering until it is found here. v2 and v3 read and write the same subscriptions: one created through either surface is listed by the other. The differences are set out in Differences from the v2 Webhooks API.
Every route on this page, reads included, needs a key that acts as an Administrator or as the workspace. In the app, subscriptions sit behind the API key page, which only Administrators can open, and the API applies the same limit. A key that acts as a Member is refused with 403 FORBIDDEN on every route, whatever permissions the Member holds. The refusal comes before anything else is checked — before the body is read or the id looked up — so such a key gets 403, never a 400 or a 404, and learns nothing about which subscriptions exist.
Every write on this page accepts an Idempotency-Key header (see Idempotency); the header matters most on Rotate a Signing Secret and Replay a Delivery. Every route is charged to the standard rate limit budget.
Endpoints
| Method | Path | Description | MCP tool |
|---|---|---|---|
GET | /v3/webhooks | List webhooks | list_webhooks |
POST | /v3/webhooks | Create a webhook | create_webhook |
GET | /v3/webhooks/{id} | Get a webhook | get_webhook |
PATCH | /v3/webhooks/{id} | Update a webhook | update_webhook |
DELETE | /v3/webhooks/{id} | Delete a webhook | delete_webhook |
POST | /v3/webhooks/{id}/rotate-secret | Rotate a signing secret | — |
GET | /v3/webhooks/{id}/deliveries | List deliveries | list_webhook_deliveries |
GET | /v3/webhooks/{id}/deliveries/{deliveryId} | Get a delivery | — |
POST | /v3/webhooks/{id}/deliveries/{deliveryId}/replay | Replay a delivery | replay_webhook_delivery |
The MCP server has no tool for rotating a secret or reading one delivery, on purpose: both answers contain things that should not end up in a model's transcript — the signing secret and the posted body. create_webhook also answers without the secret. A subscription created over MCP gets a secret somebody can see by being rotated here. See Webhook Tools.
Events and Scopes
Each subscription listens for one event. Its scope narrows which occurrences of that event it hears about, and which scopes an event accepts is fixed by the event:
| Event | Fires when | Scopes it accepts |
|---|---|---|
new_checklist | A checklist is started from a template. | Template (required) |
task_completed | A task is completed. | Task, Template, Workspace |
file_uploaded | A file is uploaded to a File Upload control. | Field (required) |
checklist_completed | A checklist becomes complete. | Template, Workspace |
comment_created | A comment is written on a task. | Task, Workspace |
task_assigned | A task is given to somebody who did not have it. | Task, Template, Workspace |
data_set.record.created | A Data Set record is added. | Workspace |
data_set.record.updated | A Data Set record's values change. | Workspace |
data_set.record.deleted | A Data Set record is deleted. | Workspace |
The event names are matched exactly, including case. They are not a naming scheme — six are snake_case and three are dotted, for historical reasons that cannot be tidied without breaking subscriptions already in place — so treat them as opaque strings. task.completed and Task_Completed are both refused.
Leave scope out for a workspace-wide subscription. new_checklist and file_uploaded have no workspace-wide form, so a subscription to either must name a scope. A scope the event does not accept is refused with 400, and the message says which scopes it does accept.
A workspace-wide comment_created subscription sends every comment anybody writes anywhere in the workspace to your URL, as the HTML it was typed in. Narrow it to a task unless that is what you want.
Scope Keys Are Template Keys
Every scope key is a key from the template, not from a checklist:
Scope type | key is | Where to find it |
|---|---|---|
Workspace | Not sent. | — |
Template | The template's key. | GET /v3/templates |
Task | A template task's key, or a standalone task's key. | The template's tasks on GET /v3/templates/{templateKey}; a standalone task's key on Standalone Tasks. |
Field | A template field's key. The field must be a File Upload control. | The fields of each task on GET /v3/templates/{templateKey} |
A task on a checklist carries the key of the template task it was made from, and so does each of its fields. So a task_completed subscription scoped to a task fires for that task in every checklist started from its template, not for one task on one checklist. There is no way to subscribe to a single occurrence of a template task. A standalone task is the exception: it has no template behind it, so its key is its own and a subscription scoped to it fires for that one task.
Every key is checked against the workspace when the subscription is created. A key that names nothing is a 404 — so is the key of an archived template, because no checklist can be started from it — and a Field key naming a control other than a File Upload is a 400.
Receiving Deliveries
The Request
When an event fires, CheckFlow sends one POST to the subscription's targetUrl:
| Header | Value |
|---|---|
Content-Type | application/json; charset=utf-8 |
CF-Delivery-Id | A GUID naming this delivery of this event to this subscription. The same value on every attempt at it, and on a replay. It is the id of the Delivery object. |
CF-Signature | t=<unix seconds>,v1=<hex> — see Verifying Signatures. Present only when the subscription has a signing secret. |
The body is the event's payload, described on Webhook Events. Two things about it are easy to trip over:
- No header names the event, and four of the nine payloads do not contain the event's name either.
new_checklistandchecklist_completedsend the same shape. Give each subscription its own target URL — a different path is enough — so that the URL tells you which subscription, and therefore which event, a delivery belongs to. - Keep the raw bytes. The signature is computed over the body exactly as sent, so read it before any JSON parsing middleware reformats it.
Responding
Answer with any 2xx status within 30 seconds. Anything else — any other status, including 4xx, a timeout, a refused connection, a TLS failure — counts as a failed attempt. The response body is ignored.
Do the work after answering rather than before: verify the signature, record the CF-Delivery-Id, put the event on a queue of your own and return 200. A receiver that does its processing inside the request is the one that times out, and a timed-out delivery is sent again.
Retries
A delivery that fails is posted again, up to five attempts in all:
| Attempt | Sent |
|---|---|
| 1 | When the event is processed. |
| 2 | 1 minute after attempt 1. |
| 3 | 5 minutes after attempt 2. |
| 4 | 25 minutes after attempt 3. |
| 5 | 2 hours 5 minutes after attempt 4. |
After the fifth failure the delivery is finished: its outcome becomes failed and nothing further happens to it on its own. There is no dead-letter queue; you can replay it by hand for up to 30 days.
- Every attempt carries the same
CF-Delivery-Idand the same body, byte for byte.CF-Signatureis computed afresh for each attempt, so itstis the time of that attempt rather than the time of the event, and a retry two hours later still passes a timestamp check. - A retry is posted to the subscription's
targetUrlas it is when the retry is made. Fixing the URL during an outage lets the remaining attempts reach the new address. - A delivery that is refused — see Target URL Rules — is never retried.
- If the subscription is paused, switched off or deleted while a retry is waiting, the retry is dropped. The delivery record is left as it was, showing
pending. - Events are delivered independently. A delivery that is being retried can arrive after a later event has been delivered, so do not rely on arrival order; the payloads' own timestamps say when things happened.
Deduplicating
Delivery is at least once. A timeout does not say whether your system committed the work before it or after, so CheckFlow sends again whenever the answer is unknown, and you will occasionally receive a delivery you have already processed. Record each CF-Delivery-Id you accept and answer a repeat with 200 without acting on it again. Automatic retries fall within about three hours of the first attempt; a replay can reuse an id up to 30 days later.
Verify the signature before checking the id. CF-Delivery-Id is not part of what is signed, so anybody who can reach your endpoint can send any id they like — and a receiver that looks up an unverified id can be talked out of processing a real delivery.
Two identical subscriptions are two subscriptions: the event is delivered to each, with a different CF-Delivery-Id on each.
Automatic Disabling
A subscription that misses ten events in a row is switched off by CheckFlow. The count is of events, not attempts: a delivery that failed four times and arrived on the fifth counts as delivered, and one that used all five attempts counts once. A refused delivery counts too, straight away.
When it happens, isActive becomes false and disabledDateTime and disabledReason are set on the Webhook object. Nothing notifies anybody — there is no email and nothing in the app — so an integration that matters should read its subscription from time to time. consecutiveFailures is the early warning: anything above 0 means events are being missed now.
To turn it back on, fix the endpoint and update the webhook with isActive: true. That clears the counter, disabledDateTime and disabledReason. So does any update that leaves the subscription active, including one that only changes targetUrl. Events missed while it was off are not sent; their delivery records remain in the history until they are 30 days old.
Pausing
isActive: false pauses a subscription. It keeps its id, event, target, scope and secret, and is skipped when the event fires. Nothing is queued while it is paused and nothing is sent when it resumes. Create a subscription with isActive: false to set it up before your receiving end is ready.
Target URL Rules
A new or changed targetUrl must be:
- an absolute
httpsURL of at most 1000 characters; - free of credentials — no
user:password@— so put anything your endpoint needs to authenticate the call in the path or query; - somewhere on the public internet. A loopback, private (RFC 1918), carrier-grade NAT, link-local or IPv6 unique-local address is refused, as is the cloud metadata address, a host name with no dot in it,
localhost, and names ending in.local,.localdomain,.internal,.intranetor.home.arpa. A host name is resolved, and one that resolves only to such addresses is refused.
A host name that does not resolve at all is accepted, so you can create a paused subscription for an endpoint that does not exist yet.
The same address check runs again on every attempt, and the connection is made to the address that passed it. A target whose name has come to resolve only to private addresses is refused: no request is made, the delivery's outcome is refused, and it is not retried. The same happens to an older subscription whose target is not an absolute http or https URL at all, or whose name resolves to no address. Subscriptions created before these rules keep their http targets and go on being delivered to.
Verifying Signatures
The Signature Header
Every delivery to a subscription that has a signing secret carries:
CF-Signature: t=1789465212,v1=5b29f9f5e2d3775dfa58ac0e2448cc69c12ae05e0dd3fb8897bce19fd8049a4b
| Part | Meaning |
|---|---|
t | When this attempt was signed, in Unix seconds. |
v1 | HMAC-SHA256, as 64 lower-case hex characters, keyed with the signing secret, over the bytes of t, a full stop and the raw request body. There can be more than one. |
The signed message is exactly:
{t}.{body}
— the decimal digits of t with no padding, a literal ., then the body as UTF-8, byte for byte as it arrived. The key is the UTF-8 bytes of the whole secret string, cfwhsec_ prefix included. Do not hex-decode the secret or strip its prefix.
During a rotation the header carries two v1 values, the newest secret's first. Accept the delivery if any of them matches.
Verification Steps
- Read the raw body before anything parses it.
- Split
CF-Signatureon commas; taketand everyv1. - Refuse the delivery if
tis more than a few minutes away from your clock. The samples below use five minutes. The timestamp is inside the signed message so that a captured delivery cannot be replayed at you later. - Compute HMAC-SHA256 of
{t}.{body}with your secret, and compare it with eachv1in constant time. - Only then look at
CF-Delivery-Id, and deduplicate on it.
Answer a delivery that fails verification with any non-2xx status. It will be retried, and if the failure was a secret you had not deployed yet, the retry will pass once you have.
Node.js
const crypto = require('crypto');
const express = require('express');
const TOLERANCE_SECONDS = 5 * 60;
function verifyCheckFlowSignature(rawBody, header, secret, nowSeconds = Math.floor(Date.now() / 1000)) {
if (!header) return false;
let timestamp = null;
const signatures = [];
for (const part of header.split(',')) {
const at = part.indexOf('=');
if (at < 0) continue;
const name = part.slice(0, at).trim();
const value = part.slice(at + 1).trim();
if (name === 't') timestamp = value;
else if (name === 'v1') signatures.push(value);
}
if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
if (Math.abs(nowSeconds - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret) // the whole secret, cfwhsec_ prefix included
.update(`${timestamp}.`)
.update(rawBody) // a Buffer holding the body as it arrived
.digest();
return signatures.some((signature) => {
const given = Buffer.from(signature, 'hex');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
const app = express();
// express.raw keeps the body as a Buffer instead of parsing it.
app.post('/hooks/checkflow/task-completed', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyCheckFlowSignature(req.body, req.get('CF-Signature'), process.env.CHECKFLOW_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const deliveryId = req.get('CF-Delivery-Id');
// If deliveryId has been processed before, answer 200 and stop here.
// Otherwise record it, queue JSON.parse(req.body) for processing, and answer straight away.
res.sendStatus(200);
});
Python
import hashlib
import hmac
import os
import time
from flask import Flask, request
TOLERANCE_SECONDS = 5 * 60
def verify_checkflow_signature(raw_body: bytes, header: str | None, secret: str, now: float | None = None) -> bool:
if not header:
return False
timestamp, signatures = None, []
for part in header.split(","):
name, sep, value = part.strip().partition("=")
if not sep:
continue
if name == "t":
timestamp = value
elif name == "v1":
signatures.append(value)
if timestamp is None or not (timestamp.isascii() and timestamp.isdigit()) or not signatures:
return False
now = time.time() if now is None else now
if abs(now - int(timestamp)) > TOLERANCE_SECONDS:
return False
# The key is the whole secret, cfwhsec_ prefix included.
expected = hmac.new(
secret.encode("utf-8"),
timestamp.encode("ascii") + b"." + raw_body,
hashlib.sha256,
).hexdigest().encode("ascii")
return any(hmac.compare_digest(expected, signature.encode("utf-8")) for signature in signatures)
app = Flask(__name__)
@app.post("/hooks/checkflow/task-completed")
def task_completed():
raw_body = request.get_data() # the bytes as they arrived; read before request.json
if not verify_checkflow_signature(raw_body, request.headers.get("CF-Signature"), os.environ["CHECKFLOW_WEBHOOK_SECRET"]):
return "", 401
delivery_id = request.headers.get("CF-Delivery-Id")
# If delivery_id has been processed before, answer 200 and stop here.
# Otherwise record it, queue the parsed body for processing, and answer straight away.
return "", 200
C#
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
public static class CheckFlowSignature
{
public static readonly TimeSpan Tolerance = TimeSpan.FromMinutes(5);
public static bool Verify(byte[] rawBody, string? header, string secret, DateTimeOffset now)
{
if (string.IsNullOrEmpty(header))
return false;
string? timestamp = null;
var signatures = new List<string>();
foreach (var part in header.Split(','))
{
var at = part.IndexOf('=');
if (at < 0)
continue;
var name = part[..at].Trim();
var value = part[(at + 1)..].Trim();
if (name == "t")
timestamp = value;
else if (name == "v1")
signatures.Add(value);
}
if (timestamp is null || signatures.Count == 0
|| !long.TryParse(timestamp, NumberStyles.None, CultureInfo.InvariantCulture, out var seconds))
return false;
if (Math.Abs(now.ToUnixTimeSeconds() - seconds) > Tolerance.TotalSeconds)
return false;
var prefix = Encoding.UTF8.GetBytes(timestamp + ".");
var message = new byte[prefix.Length + rawBody.Length];
prefix.CopyTo(message, 0);
rawBody.CopyTo(message, prefix.Length);
// The key is the whole secret, cfwhsec_ prefix included.
var expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), message);
foreach (var signature in signatures)
{
byte[] given;
try
{
given = Convert.FromHexString(signature);
}
catch (FormatException)
{
continue;
}
if (CryptographicOperations.FixedTimeEquals(given, expected))
return true;
}
return false;
}
}
In an ASP.NET Core minimal API:
app.MapPost("/hooks/checkflow/task-completed", async (HttpRequest request, IConfiguration config) =>
{
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
var body = buffer.ToArray();
if (!CheckFlowSignature.Verify(body, request.Headers["CF-Signature"], config["CheckFlow:WebhookSecret"]!, DateTimeOffset.UtcNow))
return Results.Unauthorized();
var deliveryId = request.Headers["CF-Delivery-Id"].ToString();
// If deliveryId has been processed before, answer 200 and stop here.
// Otherwise record it, queue the body for processing, and answer straight away.
return Results.Ok();
});
A Worked Example
Use these values to test your verification code. With the secret
cfwhsec_9f3a1c7e5b2d4f608a1e3c5b7d9f2a4c6e8b0d1f3a5c7e9b2d4f6a8c0e1b3d5f
this body (one line, no trailing newline)
{"eventType":"data_set.record.deleted","dataSetId":"9a3b6f88-1f2c-4a7d-9c62-1d7f5b0e4a11","dataSetName":"Suppliers","recordId":"c81f4a90-6f2b-4b8e-9a71-5d3c0e8b7a24"}
signed at t=1789465212 (2026-09-15 09:40:12 UTC) gives
CF-Signature: t=1789465212,v1=5b29f9f5e2d3775dfa58ac0e2448cc69c12ae05e0dd3fb8897bce19fd8049a4b
During a rotation, with the new secret cfwhsec_4b7e1a9d3c6f2e8b5a1d7c4f9e2b6a3d8c5f1e7b4a9d2c6f3e8b1a5d7c4f9e2b issued and the one above still in its overlap, the same delivery would carry
CF-Signature: t=1789465212,v1=b6833a341600974c6f2d0484f2d8ee4d95f721c7ee0bd06a32ff5cb3650ef9a1,v1=5b29f9f5e2d3775dfa58ac0e2448cc69c12ae05e0dd3fb8897bce19fd8049a4b
and a receiver holding either secret accepts it. Your clock must be within your tolerance of t for the check to pass, so pass a fixed "now" when testing.
The Signing Secret
A subscription created through v3 is given a secret: cfwhsec_ followed by 64 lower-case hex characters (256 random bits). The prefix is there so a secret that turns up in a log or a repository can be recognised as one.
The secret is returned once, in the 201 response to Create a Webhook, and never again by any read. Store it as soon as you receive it. If it is lost or exposed, rotate it — that is the only way to get a secret you can see. secretSetDateTime on the Webhook object tells you when the current secret was set, which is enough to check that a rotation landed.
Rotating the Secret
Rotate a Signing Secret issues a new secret and returns it. The old one does not stop at once: for 24 hours every delivery is signed with both, and CF-Signature carries two v1 values. A receiver that accepts a delivery when any v1 matches keeps working throughout, whichever of the two secrets it holds, so you can deploy the new secret at any point in those 24 hours. previousSecretExpiresDateTime on the subscription says when the overlap ends; after it, only the new secret signs.
Only one previous secret is kept. Rotating again during an overlap makes the secret from the first rotation the previous one, and the secret before that stops signing immediately.
Subscriptions Without a Secret
A subscription created through the v2 API, or before signing existed, has no secret: secretSetDateTime is absent and its deliveries carry no CF-Signature header at all. Nothing about such a delivery proves it came from CheckFlow. An unguessable URL is not authentication — URLs end up in proxy logs, screenshots and support tickets — so do not act on an unsigned delivery as if it were trustworthy.
- Give it a secret. Rotating a subscription that has none gives it one and starts signing its deliveries immediately (there is no previous secret, so no overlap). A receiver that was not checking for the header is unaffected until you switch verification on.
- Until then, treat an unsigned delivery as a notification that something changed, and read the checklist, task or record back through the API before acting on it. Keep a secret, hard-to-guess path in the target URL as well.
- Once every subscription pointing at an endpoint has a secret, make a missing
CF-Signaturea rejection.
The Webhook Object
| Field | Type | Description |
|---|---|---|
id | string (GUID) | The subscription's id, and the only way to address it. Called id rather than key because it is the same value the v2 API hands out. |
eventType | string | The event it listens for — one of the nine names in Events and Scopes. |
targetUrl | string | Where deliveries are posted. Subscriptions created before the Target URL Rules are returned exactly as stored, which may be http or not a URL at all. |
source | string | A free label, at most 20 characters, such as zapier or api. It changes nothing about delivery; it lets an integration that creates subscriptions find its own again with the source filter. |
isActive | boolean | Whether it is delivering. false when paused by a person or switched off by CheckFlow. |
consecutiveFailures | integer | How many events in a row have failed to reach this subscription; 0 after one arrives. Ten switches it off. |
disabledDateTime | string | When CheckFlow switched it off. Absent when it is active, and absent when a person paused it — which is how you tell the two apart. |
disabledReason | string | Why CheckFlow switched it off, in a sentence. Absent under the same conditions as disabledDateTime. |
scope | Scope | What the subscription is narrowed to. |
createdDateTime | string | When it was created. |
secret | string | The signing secret. Present only on the response to Create a Webhook and Rotate a Signing Secret. |
secretSetDateTime | string | When the current secret was set. Absent when the subscription has no secret. |
previousSecretExpiresDateTime | string | When the previous secret stops signing. Present only while a rotation is in its 24-hour overlap. |
{
"id": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"isActive": true,
"consecutiveFailures": 0,
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"createdDateTime": "2026-09-15T09:12:44.187Z",
"secretSetDateTime": "2026-09-15T09:12:44.187Z"
}
A subscription CheckFlow has switched off reads like this:
{
"id": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"isActive": false,
"consecutiveFailures": 10,
"disabledDateTime": "2026-09-21T16:02:31.460Z",
"disabledReason": "Switched off automatically after 10 consecutive events failed to be delivered, the last of them task_completed. Fix the endpoint and reactivate the subscription, or change its target URL.",
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"createdDateTime": "2026-09-15T09:12:44.187Z",
"secretSetDateTime": "2026-09-15T09:12:44.187Z"
}
The Scope Object
| Field | Type | Description |
|---|---|---|
type | string | Workspace, Template, Task or Field. Matched without regard to case on the way in; always returned in this spelling. |
key | string (GUID) | The template, task or field key — see Scope Keys Are Template Keys. Absent for Workspace. |
The v2 API can store a task_completed subscription with both a task key and a template key. Such a subscription is delivered on both — for that task, and for every task of the template — but this object can only show one scope, so it reports the narrower one (field, then task, then template). Delete it and create it again through v3 to make it mean one thing; the scope cannot be changed by an update.
The Delivery Object
One delivery of one event to one subscription, with every attempt at it recorded on the same row.
| Field | Type | Description |
|---|---|---|
id | string (GUID) | The delivery's id — the CF-Delivery-Id header your endpoint was sent. |
webhookId | string (GUID) | The subscription it was delivered for. |
eventType | string | The event it carried. |
outcome | string | delivered — your endpoint answered 2xx. failed — requests were made and none got a 2xx, and CheckFlow has stopped trying. refused — no request was made, because the target broke the Target URL Rules at the time of sending; fix the target, since retrying will not help. pending — not delivered yet and another attempt is queued. |
statusCode | integer | The status your endpoint answered the last attempt with. Absent when nothing answered — a timeout, a connection failure, or a refused delivery. |
failure | string | Why the last attempt did not arrive, in a sentence (at most 1000 characters). Absent when it arrived. |
attemptCount | integer | How many requests have been made, counting one in progress. 1–5 for automatic delivery; higher once the delivery has been replayed. A pending row showing 3 has made three requests and is waiting to make the fourth. |
durationMs | integer | How long the last attempt took, measured from CheckFlow's side, in milliseconds. Absent when no request was made. |
createdDateTime | string | When the event was delivered for the first time. It does not move on a retry or replay, so the delivery keeps its place in the list. |
lastAttemptDateTime | string | When the last attempt was made. The gap from createdDateTime is how long your endpoint has been failing. |
targetUrl | string | Where the last attempt was posted, which may not be where the subscription points today. Only on Get a Delivery and Replay a Delivery. |
payload | string | The body that was posted, as a string holding the exact bytes, not as a nested object — so you can re-verify a signature over it. Only on Get a Delivery. |
{
"id": "6f5c2a4e-1d3b-4f8a-9c71-2e0b5a7d4c19",
"webhookId": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"outcome": "pending",
"statusCode": 503,
"failure": "The subscriber answered 503 Service Unavailable.",
"attemptCount": 2,
"durationMs": 184,
"createdDateTime": "2026-09-15T09:40:13.203Z",
"lastAttemptDateTime": "2026-09-15T09:41:14.870Z"
}
Delivery records are kept for 30 days and then deleted by a nightly sweep. There is no archive: after that, the history cannot say whether an event was sent. If you need a permanent record, keep one at your end — the CF-Delivery-Id you processed and when is enough to join it back to this history while it lasts.
List Webhooks
Returns the workspace's subscriptions, newest first. This is the way to audit where the workspace's data is being sent, including subscriptions created through v2.
GET /v3/webhooks
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
eventType | query | string | No | Only subscriptions to this event. One of the nine names in Events and Scopes, matched exactly, including case. |
source | query | string | No | Only subscriptions with this label, matched in full without regard to case. ALL means the label ALL (unlike v2, where it means no filter). |
isActive | query | boolean | No | true for subscriptions that are delivering, false for paused and switched-off ones. |
sort | query | string | No | createdDateTime, eventType or targetUrl, optionally with :asc or :desc. Default createdDateTime:desc. Ties are ordered by id. |
after | query | string | No | The nextCursor from the previous page. See Pagination. |
pageSize | query | integer | No | 1–100, default 50. A value outside the range is replaced by the default rather than refused. |
Example
GET https://api.checkflow.io/v3/webhooks?eventType=task_completed
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
{
"items": [
{
"id": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"isActive": true,
"consecutiveFailures": 0,
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"createdDateTime": "2026-09-15T09:12:44.187Z",
"secretSetDateTime": "2026-09-15T09:12:44.187Z"
},
{
"id": "e2b94f17-6c0a-4d38-b5e1-8a7f3c29d604",
"eventType": "task_completed",
"targetUrl": "http://legacy.acme.example/checkflow",
"source": "zapier",
"isActive": true,
"consecutiveFailures": 0,
"scope": {
"type": "Workspace"
},
"createdDateTime": "2025-11-04T14:20:05.550Z"
}
],
"hasMore": false,
"total": 2
}
The second subscription was created through v2: it has no secretSetDateTime, so its deliveries are unsigned.
Responses
| Status | Code | When |
|---|---|---|
200 | — | The page of subscriptions. An empty items when the workspace has none. |
400 | VALIDATION_ERROR | field is eventType for a name that is not one of the nine, isActive for a value other than true or false, sort for an unknown sort field, or after for a cursor that cannot be read or was issued for a different query. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
Create a Webhook
Subscribes a URL to an event. Deliveries start as soon as this answers, unless you send isActive: false.
POST /v3/webhooks
Parameters
This endpoint takes no parameters.
Request Body
{
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"isActive": true
}
| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | One of the nine names in Events and Scopes, matched exactly, including case. |
targetUrl | string | Yes | Where to post. Must follow the Target URL Rules. |
source | string | No | A label, at most 20 characters. Default api. |
scope | Scope | No | What to narrow the subscription to. Leave it out for the whole workspace — which new_checklist and file_uploaded do not allow. |
isActive | boolean | No | Whether to start delivering now. Default true. |
Example
POST https://api.checkflow.io/v3/webhooks
X-API-KEY: your-api-key-here
Content-Type: application/json
Idempotency-Key: 0b7e5c1a-invoice-review-task-completed
{
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
}
}
HTTP/1.1 201 Created
{
"id": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"isActive": true,
"consecutiveFailures": 0,
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"createdDateTime": "2026-09-15T09:12:44.1873526Z",
"secret": "cfwhsec_9f3a1c7e5b2d4f608a1e3c5b7d9f2a4c6e8b0d1f3a5c7e9b2d4f6a8c0e1b3d5f",
"secretSetDateTime": "2026-09-15T09:12:44.1873526Z"
}
Responses
| Status | Code | When |
|---|---|---|
201 | — | The subscription, with secret. Store the secret now. |
400 | VALIDATION_ERROR | No body. field is eventType when it is missing or not one of the nine; targetUrl when it is missing, longer than 1000 characters, not an absolute URL, not https, carries credentials or points inside a private network; source when it is longer than 20 characters; scope when the event requires a scope and none was sent; scope.type for an unknown type, a type the event does not accept, or a key sent without a type; scope.key for a key on a Workspace scope, a missing or malformed key, or a Field key naming a control that is not a File Upload. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | TEMPLATE_NOT_FOUND | A Template scope key names no unarchived template of this workspace. |
404 | TASK_NOT_FOUND | A Task scope key names neither a task in the latest version of a template nor a standalone task of this workspace. |
404 | FIELD_NOT_FOUND | A Field scope key names no template field of this workspace. |
Notes
- There is no duplicate check. Two identical subscriptions are two subscriptions, and each receives every event.
- A key from another workspace is a
404, the same as one that does not exist. - The
201response is stored against anIdempotency-Keylike any other, so a retry with the same key returns the same secret rather than creating a second subscription. create_webhookover MCP returns the subscription withoutsecret.
Get a Webhook
Returns one subscription by the id the create returned. It never includes secret.
GET /v3/webhooks/{id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (GUID) | Yes | The subscription's id. |
Example
GET https://api.checkflow.io/v3/webhooks/7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
{
"id": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"isActive": true,
"consecutiveFailures": 2,
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"createdDateTime": "2026-09-15T09:12:44.187Z",
"secretSetDateTime": "2026-09-15T09:12:44.187Z"
}
consecutiveFailures of 2 means the last two events did not reach the endpoint.
Responses
| Status | Code | When |
|---|---|---|
200 | — | The subscription. |
400 | VALIDATION_ERROR | field is id: the id is not a GUID. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | WEBHOOK_NOT_FOUND | This workspace has no subscription with that id — including one that belongs to another workspace. |
Update a Webhook
Changes where a subscription delivers, its label, or whether it delivers. Use it to pause, resume or reactivate a subscription, or to point it at a new endpoint.
PATCH /v3/webhooks/{id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (GUID) | Yes | The subscription's id. |
Request Body
A partial update: only the fields you send are changed, and a field you leave out keeps its value. None of the three can be cleared — null is refused for each. Send at least one.
{
"targetUrl": "https://hooks.acme.example/checkflow/v2/task-completed",
"isActive": true
}
| Field | Type | Required | Description |
|---|---|---|---|
targetUrl | string | No | Where to deliver now. Same rules as on create. |
source | string | No | The label. At most 20 characters; an empty label is refused. |
isActive | boolean | No | false pauses; true resumes or reactivates. |
The event type and the scope cannot be changed: a subscription whose event or scope changed would be a different subscription wearing the same id. Delete it and create the one you want. A body containing only eventType or scope changes nothing and is refused.
Example
PATCH https://api.checkflow.io/v3/webhooks/7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47
X-API-KEY: your-api-key-here
Content-Type: application/json
{
"isActive": true
}
HTTP/1.1 200 OK
{
"id": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"isActive": true,
"consecutiveFailures": 0,
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"createdDateTime": "2026-09-15T09:12:44.187Z",
"secretSetDateTime": "2026-09-15T09:12:44.187Z"
}
This reactivated a subscription CheckFlow had switched off: consecutiveFailures, disabledDateTime and disabledReason are cleared.
Responses
| Status | Code | When |
|---|---|---|
200 | — | The subscription as it now reads. |
400 | VALIDATION_ERROR | No body, or a body with none of the three fields (no field). field is id for a malformed id; targetUrl for null or a target that breaks the Target URL Rules; source for null, an empty label or one longer than 20 characters; isActive for null. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | WEBHOOK_NOT_FOUND | This workspace has no subscription with that id, or it was deleted while the update was being made. |
Notes
- Setting
isActiveto the value it already has is a200. - Any update that leaves the subscription active resets
consecutiveFailuresand clearsdisabledDateTimeanddisabledReason. - Changing
targetUrldoes not change the secret. - Deliveries still being retried go to the new
targetUrlfrom their next attempt.
Delete a Webhook
Removes a subscription. Nothing else in the workspace is affected. To stop deliveries but keep the subscription, update it with isActive: false instead.
DELETE /v3/webhooks/{id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (GUID) | Yes | The subscription's id. |
Example
DELETE https://api.checkflow.io/v3/webhooks/7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47
X-API-KEY: your-api-key-here
Responds 204 No Content with no body.
Responses
| Status | Code | When |
|---|---|---|
204 | — | Deleted. |
400 | VALIDATION_ERROR | field is id: the id is not a GUID. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | WEBHOOK_NOT_FOUND | This workspace has no subscription with that id. |
Notes
- An event that fired just before the delete can still be delivered just after it.
- Waiting retries for the subscription's deliveries are dropped.
Rotate a Signing Secret
Issues a new signing secret and returns it, in this response only. Use it when a secret is lost or may have been exposed, or to give a secret to a subscription that has none.
POST /v3/webhooks/{id}/rotate-secret
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (GUID) | Yes | The subscription's id. |
Request Body
This endpoint takes no request body.
Example
POST https://api.checkflow.io/v3/webhooks/7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47/rotate-secret
X-API-KEY: your-api-key-here
Idempotency-Key: rotate-7c1d4e9a-2026-09-22
HTTP/1.1 200 OK
{
"id": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed",
"source": "api",
"isActive": true,
"consecutiveFailures": 0,
"scope": {
"type": "Template",
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14"
},
"createdDateTime": "2026-09-15T09:12:44.187Z",
"secret": "cfwhsec_4b7e1a9d3c6f2e8b5a1d7c4f9e2b6a3d8c5f1e7b4a9d2c6f3e8b1a5d7c4f9e2b",
"secretSetDateTime": "2026-09-22T08:05:17.6613092Z",
"previousSecretExpiresDateTime": "2026-09-23T08:05:17.6613092Z"
}
Responses
| Status | Code | When |
|---|---|---|
200 | — | The subscription, with the new secret. |
400 | VALIDATION_ERROR | field is id: the id is not a GUID. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | WEBHOOK_NOT_FOUND | This workspace has no subscription with that id, or it was deleted during the rotation. |
Notes
- The old secret keeps signing for 24 hours alongside the new one — see Rotating the Secret.
previousSecretExpiresDateTimeis absent when the subscription had no secret before. - Each call issues a new secret and discards the one before. If a response is lost, the secret in it is lost with it. Send an
Idempotency-Key: a retry with the same key within 24 hours returns the stored response, secret included, instead of rotating again. Without a key, rotate again rather than assuming the first call failed. - There is no MCP tool for this route.
List Deliveries
Returns every delivery made for one subscription in the last 30 days, newest first: what was sent, what came back and how long it took. Start here when a webhook "is not working" — an empty list means the subscription is not firing at all (check its event and scope), while a list of failed rows means it is firing into an endpoint that will not accept it.
GET /v3/webhooks/{id}/deliveries
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (GUID) | Yes | The subscription's id. |
outcome | query | string | No | Only deliveries with this outcome: delivered, failed, refused or pending, matched without regard to case. |
after | query | string | No | The nextCursor from the previous page. See Pagination. |
pageSize | query | integer | No | 1–100, default 50. A value outside the range is replaced by the default rather than refused. |
There is no sort; the order is always newest createdDateTime first.
Example
GET https://api.checkflow.io/v3/webhooks/7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47/deliveries?pageSize=3
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
{
"items": [
{
"id": "6f5c2a4e-1d3b-4f8a-9c71-2e0b5a7d4c19",
"webhookId": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"outcome": "pending",
"statusCode": 503,
"failure": "The subscriber answered 503 Service Unavailable.",
"attemptCount": 2,
"durationMs": 184,
"createdDateTime": "2026-09-15T09:40:13.203Z",
"lastAttemptDateTime": "2026-09-15T09:41:14.870Z"
},
{
"id": "0d8b3f5e-7a21-4c96-8e4b-1f9a6c2d7e30",
"webhookId": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"outcome": "failed",
"failure": "The subscriber did not answer within 30 seconds.",
"attemptCount": 5,
"durationMs": 30004,
"createdDateTime": "2026-09-14T15:02:40.117Z",
"lastAttemptDateTime": "2026-09-14T17:38:53.990Z"
},
{
"id": "b3e7a9c1-4f26-4d0b-9a85-6c1e2f7d8b43",
"webhookId": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"outcome": "delivered",
"statusCode": 200,
"attemptCount": 1,
"durationMs": 212,
"createdDateTime": "2026-09-14T11:26:08.643Z",
"lastAttemptDateTime": "2026-09-14T11:26:08.643Z"
}
],
"nextCursor": "eyJ2IjoxLCJzIjoiY3JlYXRlZERhdGVUaW1lOmRlc2MiLCJvIjozfQ",
"hasMore": true
}
The rows carry no targetUrl or payload; Get a Delivery has both.
Responses
| Status | Code | When |
|---|---|---|
200 | — | The page of deliveries. A subscription that has never fired returns an empty page. |
400 | VALIDATION_ERROR | field is id for a malformed id, outcome for a value that is not one of the four, or after for a cursor that cannot be read or was issued for a different query. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | WEBHOOK_NOT_FOUND | This workspace has no subscription with that id. |
Notes
totalis never returned on this list, and a page can hold fewer thanpageSizerows whilehasMoreis stilltrue. Keep followingnextCursoruntilhasMoreisfalse.- The first page fixes the moment the history is read at, and the cursor carries it: deliveries made while you page do not push rows onto later pages. Start again from the first page to see them.
outcome=pendingcan skip a row: a pending delivery that finishes while you are paging leaves the filtered set and moves the rows after it up. When you need every row, page without the filter and readoutcomeyourself.outcome=failedmeans "given up on". It is the filter to use when checking whether events have been lost.
Get a Delivery
Returns one delivery with the body that was posted and where it was posted. deliveryId is the CF-Delivery-Id header your endpoint received, so a receiver that logs that header can come straight here.
GET /v3/webhooks/{id}/deliveries/{deliveryId}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (GUID) | Yes | The subscription's id. |
deliveryId | path | string (GUID) | Yes | The delivery's id — the CF-Delivery-Id header. |
Example
GET https://api.checkflow.io/v3/webhooks/c05a8e2d-3b71-4f9c-a6e4-7d2b1f8c0e95/deliveries/4a9e1c7b-2d5f-4b80-93e6-0f7c1a2b8d54
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
{
"id": "4a9e1c7b-2d5f-4b80-93e6-0f7c1a2b8d54",
"webhookId": "c05a8e2d-3b71-4f9c-a6e4-7d2b1f8c0e95",
"eventType": "data_set.record.deleted",
"outcome": "delivered",
"statusCode": 200,
"attemptCount": 1,
"durationMs": 97,
"createdDateTime": "2026-09-15T09:40:12.503Z",
"lastAttemptDateTime": "2026-09-15T09:40:12.503Z",
"targetUrl": "https://hooks.acme.example/checkflow/supplier-deleted",
"payload": "{\"eventType\":\"data_set.record.deleted\",\"dataSetId\":\"9a3b6f88-1f2c-4a7d-9c62-1d7f5b0e4a11\",\"dataSetName\":\"Suppliers\",\"recordId\":\"c81f4a90-6f2b-4b8e-9a71-5d3c0e8b7a24\"}"
}
payload is a JSON string holding the exact body. Decode the string, then compute HMAC-SHA256 of {t}.{payload} with the t from the CF-Signature your endpoint received, and it matches that header's v1.
Responses
| Status | Code | When |
|---|---|---|
200 | — | The delivery, with targetUrl and payload. |
400 | VALIDATION_ERROR | field is id or deliveryId: that id is not a GUID. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | WEBHOOK_NOT_FOUND | This workspace has no subscription with that id. |
404 | NOT_FOUND | The subscription has no delivery with that id — including a delivery of another subscription and one older than 30 days. |
Notes
- The payload is the workspace's own data — task names, form answers, comments. There is no MCP tool for this route for that reason.
Replay a Delivery
Posts a delivery again, now, and answers with the result. Use it to recover an event your endpoint lost or rejected, after fixing the endpoint.
POST /v3/webhooks/{id}/deliveries/{deliveryId}/replay
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (GUID) | Yes | The subscription's id. |
deliveryId | path | string (GUID) | Yes | The delivery's id — the CF-Delivery-Id header. |
Request Body
This endpoint takes no request body.
Example
POST https://api.checkflow.io/v3/webhooks/7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47/deliveries/0d8b3f5e-7a21-4c96-8e4b-1f9a6c2d7e30/replay
X-API-KEY: your-api-key-here
Idempotency-Key: replay-0d8b3f5e-1
HTTP/1.1 200 OK
{
"id": "0d8b3f5e-7a21-4c96-8e4b-1f9a6c2d7e30",
"webhookId": "7c1d4e9a-2b8f-4a63-9e05-3f6a1b2c8d47",
"eventType": "task_completed",
"outcome": "delivered",
"statusCode": 200,
"attemptCount": 6,
"durationMs": 231,
"createdDateTime": "2026-09-14T15:02:40.117Z",
"lastAttemptDateTime": "2026-09-15T10:03:27.4410912Z",
"targetUrl": "https://hooks.acme.example/checkflow/task-completed"
}
A 200 means the attempt was made, not that it arrived. Read outcome.
Responses
| Status | Code | When |
|---|---|---|
200 | — | The delivery after the attempt, with targetUrl. |
400 | VALIDATION_ERROR | field is id or deliveryId: that id is not a GUID. |
403 | FORBIDDEN | The key acts as a Member. Webhook routes need a key that acts as an Administrator or as the workspace. |
404 | WEBHOOK_NOT_FOUND | This workspace has no subscription with that id. |
404 | NOT_FOUND | The subscription has no delivery with that id, or it is older than 30 days. |
409 | CONFLICT | The subscription is not active — paused, or switched off by CheckFlow. Set isActive to true first. |
409 | CONFLICT | The delivery is still pending: a retry is already queued for it, and a replay alongside would deliver it twice. Wait for its outcome. |
Notes
- It is the same delivery, not a new event. The body is the bytes posted the first time, read back from the record, and
CF-Delivery-Idis the original id. A receiver that deduplicates on the id will treat it as a repeat, so replay only what your endpoint did not process.CF-Signatureis computed afresh, with a currentt, under the subscription's current secrets. - It waits for your endpoint. The request takes as long as the attempt, up to the 30-second delivery timeout; nothing is queued.
- It is one attempt. A failed replay is not retried. It changes neither
consecutiveFailuresnor the subscription's active state, whatever the result. - The record is rewritten, not added to:
attemptCountgoes up — past five — andlastAttemptDateTimemoves, whilecreatedDateTimedoes not. - The replay goes to the subscription's current
targetUrl, which is whattargetUrlon the response shows. After retargeting a subscription, a replay delivers the old event to the new address. - The response has no
payload; Get a Delivery is where to read it. - Without an
Idempotency-Key, two replay requests are two deliveries — including two sent at the same moment, because the pending check is not a lock.
Differences from the v2 Webhooks API
The v2 webhook routes (/api/web-hook/subscriptions, /subscribe, /unsubscribe) still work and manage the same subscriptions. Payloads are identical for a v2 and a v3 subscription to the same event: each event's body is built once and posted to every matching subscription.
| v2 | v3 | |
|---|---|---|
| Events | new_checklist, task_completed, file_uploaded and the three Data Set events. | Those six, plus checklist_completed, comment_created and task_assigned. |
| Scope | Loose templateKey, taskKey and taskContentKey parameters. A task_completed subscription given a task key and a template key stores both and fires on both. | One scope with one type and one key; which types an event accepts is enforced. |
| Keys | Not checked. A mistyped key is stored and never fires. | Checked; a key naming nothing is a 404. |
| Target URL | Not checked. | Must be https, without credentials, and outside private networks. The address check applies at delivery to every subscription, v2 ones included. |
source | Required; omitting it is a 500. ALL means "no filter" when listing. | Optional, default api. ALL is an ordinary label. |
| Signing | No secret; deliveries are unsigned. | A secret on create, CF-Signature on every delivery, rotation with a 24-hour overlap. Rotating a v2 subscription through v3 gives it a secret. |
| Pausing | Not available. | isActive through Update a Webhook. |
| Unsubscribe | Answers Success for any id, even one that does not exist. | 404 WEBHOOK_NOT_FOUND. |
| List | 404 when there are no subscriptions. | An empty page. |
| Delivery history, replay | Not available. | List Deliveries, Get a Delivery, Replay a Delivery. |
Retries, the CF-Delivery-Id header, automatic disabling after ten missed events and the 30-day delivery history apply to every subscription however it was created.
Related Pages
- Webhook Events — what fires each of the nine events, and the exact payload each one sends.
- Webhook Tools — managing subscriptions and reading delivery history from an MCP client.
- Templates — reading the template, task and field keys a scope needs.
- Idempotency — making a secret rotation or a replay safe to retry.
- Webhooks (v2) — the older subscription routes this page replaces.