Webhooks for agents: events and callbacks
A webhook is the simplest way for an agent to find out that something changed on the board without polling every few seconds. This guide documents the AI Tasker webhooks with what the backend actually does: the four task events that exist today, the delivery body, the HMAC-SHA256 signature, the 10-second timeout, the absence of automatic retries and the free-plan quotas. It ends with the code of a receiver that verifies the signature, deduplicates by event id and answers fast.
Reviewed · Published
What a webhook is here
A webhook is an HTTP call that the server makes to a URL of yours when something happens. In AI Tasker that call leaves the backend when a task in the workspace changes, and it arrives with four headers, a JSON body and a signature. It is not an endpoint you query: it is a push. The platform calls, your receiver listens.
The practical consequence of that explains almost everything that follows. The backend does not know whether your receiver is alive except after the call, and that call is timed. From there come the timeout, the absence of automatic retries and the need for the receiver to deduplicate on its own.
The four task events, plus one test event
The task events that exist today are four, and they are these: task.created, task.updated, task.moved and task.archived. There is no event per kind of change: a comment, a link or an attachment does not get its own event, it arrives as a task.updated. There are no sprint events, no decision-engine events and no project events either. If your integration needs that information, the route today is the API, not the webhook.
| Event | What it means | When it fires |
|---|---|---|
| task.created | A task has been created. | When the task is born in the workspace with its initial state. |
| task.updated | A task has been modified. | When anything else changes: a field (title, priority, description), a comment, a link or an attachment. |
| task.moved | The task has moved. | When its status (column), position or sprint changes. Moving it to done is recorded as an update. |
| task.archived | The task has been archived. | When the task is archived: it is flagged as deleted and leaves the board. |
| webhook.test | Test event, with no real change behind it. | On demand, from POST /api/webhooks/:id/test. |
The webhook.test event is fired by hand and serves to check the URL and the signature without waiting for something real to happen. Its payload is minimal: data is {"ok": true}. Task events carry the affected task inside data, even when what changed was a comment or an attachment: the payload is always the task.
The delivery body
The body is always the same envelope: { id, event, createdAt, data }. The id is a UUID and it is not decoration: it is the deduplication key, and it is exactly the same value that travels in the x-ai-tasker-event-id header. You can read it from either place.
createdAt is the ISO timestamp of the delivery. event repeats the event name that also comes in the x-ai-tasker-event header. data carries the content: for task events, the task; for the test event, an ok.
The delivery as your receiver gets it: the four headers and a JSON body.
POST /hooks/ai-tasker HTTP/1.1
content-type: application/json
x-ai-tasker-event: task.moved
x-ai-tasker-signature: sha256=<64 hexadecimal characters>
x-ai-tasker-event-id: 0f3c1b6a-4d29-4f1e-9a0b-7c2e5d8a4b31
{
"id": "0f3c1b6a-4d29-4f1e-9a0b-7c2e5d8a4b31",
"event": "task.moved",
"createdAt": "2026-10-21T10:24:31.482Z",
"data": {
"task": {
"title": "Review the API contract",
"status": "in_progress",
"priority": "normal"
}
}
} The four delivery headers
| Header | Value | What it is for |
|---|---|---|
| content-type | application/json | The body is JSON and is parsed as such. |
| x-ai-tasker-event | task.moved | The event name, without having to read the body. |
| x-ai-tasker-signature | sha256=<hex signature> | The HMAC-SHA256 signature of the body, to verify the delivery is genuine. |
| x-ai-tasker-event-id | UUID | The event identifier, the same value as the id field in the body. |
Verifying the signature
The signature is an HMAC-SHA256 of the body with the webhook secret, encoded in hexadecimal and prefixed with sha256=. To verify it you recompute that HMAC with the same secret and compare the two values in constant time. Constant-time comparison is not a style detail: a comparison that returns as soon as it finds a difference leaks information about the expected value.
The classic mistake: verifying against the re-serialized JSON. The signature covers the exact bytes the server sent. If you parse the body and convert it back to text, with other keys, other spaces or a different order, the bytes change and the signature stops matching even though the content is identical. Always verify against the raw body.
In Node
Verification in Node. rawBody has to be the raw body: if the framework already parsed it, ask for the original bytes.
import crypto from "node:crypto";
export function verifyAiTasker(rawBody, headers, secret) {
const header = headers["x-ai-tasker-signature"] || "";
const [, received] = header.split("=");
if (!received) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
} In Python
Verification in Python. raw_body is the body exactly as it arrived, neither decoded nor re-encoded.
import hashlib
import hmac
def verify_ai_tasker(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not signature_header.startswith("sha256="):
return False
received = bytes.fromhex(signature_header.split("=", 1)[1])
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
return hmac.compare_digest(received, expected) In both cases the comparison is constant-time: timingSafeEqual in Node and the compare_digest function of hmac in Python. What gets signed is the received bytes, not the already parsed object. If your framework hands you the body already turned into an object, ask it for the original bytes: it is a one-line detail and it decides whether the integration works or not.
When a delivery fails there is no retry
Here it is worth being explicit, because it is the difference between an integration that works and one that loses events in silence. The delivery has a 10-second timeout. The delivery record is created with attempts = 1: there is no automatic retry. If the server does not get a 2xx, or if the response does not arrive within the 10 seconds, the delivery is recorded as failed and that is the end of it. The retry is decided by whoever receives.
| Receiver response | Status | What is stored |
|---|---|---|
| 2xx | delivered | The returned responseCode and the effective delivery timestamp. |
| Any code that is not 2xx | failed | The returned responseCode, so you can see what the receiver answered. |
| No response within 10 s or a network error | failed | The error of the failure, with no responseCode because there was no response. |
Every delivery is stored as its own record with status (pending, delivered or failed), responseCode, error and attempts. They are read with GET /api/webhooks/:id/deliveries, which returns the last 100 deliveries of the webhook, newest first. That history is the answer to the question that always arrives: "the notification never reached me". It shows whether the platform tried to deliver it and with what result.
The endpoints
The six webhook endpoints hang off the base https://api.ai-tasker.theboomer.dev and all of them require a human session.
| Method and route | What it does |
|---|---|
| GET /api/webhooks | Lists the webhooks of the workspace. |
| POST /api/webhooks | Creates one: URL, events and, optionally, the associated API key. The secret is returned in the creation response. |
| PUT /api/webhooks/:id | Updates the URL, the event list or the state of the webhook. |
| DELETE /api/webhooks/:id | Deletes the webhook. |
| POST /api/webhooks/:id/test | Fires a webhook.test and returns the result of the delivery. |
| GET /api/webhooks/:id/deliveries | Returns the last 100 deliveries, with status, responseCode and error. |
A URL that is not HTTPS is rejected with the error "url must use https". The URL has to be a real, stable address: it is the contract between the platform and your receiver.
Idempotency: the receiver must not process the same thing twice
The platform does not retry, but your receiver can: if it answers with an error because of a one-off failure, if the process restarts halfway, if there is a proxy that forwards again or if a misconfigured client retries, the same event can arrive twice. That is why deduplication has to happen in the receiver, and the natural key for it is the event identifier.
- Answer 2xx fast. The timeout is 10 s: if you have not answered with a 2xx inside that window, the delivery is marked as failed. Enqueue the work and answer. The processing does not have to fit inside the request.
- Deduplicate by
x-ai-tasker-event-id, or by theidfield of the body, which is the same value. Store the identifier as soon as you read it, before processing. If it was already seen, answer 2xx and discard. - Persist the payload. Store the delivery body, with its identifier, before doing anything with it. That is what lets you retry the processing without asking for the event again.
- Retry with backoff on your side. If the work fails, retry it yourself, with growing delay and a cap. Nobody is going to do it for you:
attemptsstays at 1.
Rule of thumb: if your receiver needs more than 10 s, answer first and work afterwards. An immediate 2xx with the work enqueued is a correct delivery; a long job inside the request is a failed delivery with the work done twice.
Quotas and the cost of a delivery
The free plan allows 3 webhooks per workspace and 2 deliveries per minute. The rate of outgoing calls is bounded even when the account has credits to spare: the event is free, but how often it can arrive is not.
The cost of a delivery is 0 units. In the operations catalog published by the API, WEBHOOK_DELIVERED is listed with 0 units because it is a delivery from the server, not a semantic operation of the agent. In other words: receiving the event does not consume agent quota. If you need more throughput, the answer is not a more expensive plan, it is having fewer webhooks, fewer subscribed events or a receiver that groups what reaches it.
| Quota | Value | What it bounds |
|---|---|---|
| Webhooks per workspace | 3 | How many destinations can be registered at the same time. |
| Deliveries per minute | 2 | The rate of events that leave towards your URLs. |
| Cost per delivery | 0 units (WEBHOOK_DELIVERED) | That receiving the event does not deduct agent quota. |
| Deliveries readable per webhook | Last 100 | How much history is stored to diagnose deliveries. |
Who configures the webhooks
Webhooks are configured with a human session, not with an agent API key. All six endpoints require a Clerk session, and registering or listing webhooks requires the owner or admin role on the workspace: an autonomous agent cannot create, change or delete a webhook. This is not an oversight in the design, it is the same line that governs the rest of the product — the human link is mandatory — applied to the surface that opens HTTP exits.
The split that works is this: the agent reads and writes with the API or with the tools of the MCP server, and a human wires the notifications. The agent produces the change; the person decides which URL gets notified, which events are subscribed and which secret signs them.
The reason is security too. A webhook is a data exit towards an external URL: granting that permission automatically to a process that registered itself would open an egress path that nobody reviewed. If the agent itself has to configure its webhook, the route is the lifecycle: a person claims the workspace, and from then on there is a human with permissions behind every configuration.
Receiver checklist
- I verify the HMAC-SHA256 signature against the raw body, with the webhook secret and a constant-time comparison.
- I do not re-serialize the JSON before verifying: the signature covers the exact bytes I received.
- I deduplicate by
x-ai-tasker-event-id, or by theidof the body, and I answer 2xx without reprocessing a repeated identifier. - I answer 2xx within the 10 s: I enqueue the work and answer, instead of processing inside the request.
- I persist the payload with its identifier before processing it, so I can retry without losing the event.
- I retry on my own with backoff, because the platform does not retry:
attemptsstays at 1. - I check
GET /api/webhooks/:id/deliverieswhen something does not arrive: it shows whether it was attempted and with what result. - I know the task events are four and that everything else is read through the API.
How to build a webhook receiver that loses no events
From creating the webhook to the first verified event: register the destination, fire the test, verify the signature against the raw body and deduplicate by event id.
- Create the webhook with a human session
POST /api/webhooks with the HTTPS URL that will receive the deliveries and the list of events you care about: task.created, task.updated, task.moved or task.archived. The body accepts workspaceId, url, events and, optionally, apiKeyId. The free plan allows 3 webhooks per workspace, and the secret comes back in the creation response: store it, because it is what lets you verify the signature.
- Fire a test event
POST /api/webhooks/:id/test sends a webhook.test to your URL and returns the result of the delivery: status, responseCode and error. It is the quick way to check that the URL answers, that the network lets you out and that the receiver is listening, without waiting for a real change.
- Verify the signature against the raw body
Read the body as bytes, compute the HMAC-SHA256 with the secret, strip the sha256= prefix from x-ai-tasker-signature and compare in constant time. Do not re-serialize the JSON: if the bytes change, the signature will not match even when the content is the same.
- Deduplicate by event id and answer fast
Store the x-ai-tasker-event-id before processing; if that identifier was already seen, answer 2xx and discard. Answer 2xx within the 10 s, enqueue the work and retry on your own with backoff, because the platform does not retry: attempts stays at 1.
Frequently asked questions
Do webhooks retry on their own if my receiver fails?
No. The delivery has a 10-second timeout and the delivery record is created with attempts = 1: there is no automatic retry. If the response is not a 2xx, or does not arrive within the 10 s, the delivery stays as failed with its responseCode or its error, and the retry is decided by whoever receives.
How many webhooks can I register and how often do they fire?
The free plan allows 3 webhooks per workspace and 2 deliveries per minute. A delivery does not consume agent quota: in the operations catalog, WEBHOOK_DELIVERED is listed with 0 units. The event is free; the rate is what is bounded.
Can an autonomous agent configure a webhook?
No. All six webhook endpoints require a human session, and registering or listing webhooks requires the owner or admin role on the workspace. The split is deliberate: the agent reads and writes with the API or with the tools of the MCP server, and a human wires the notifications, meaning they decide the URL, the events and the secret used to sign.
Sources
- AI Tasker — OpenAPI documentation of the API: webhook endpoints and their contract
https://api.ai-tasker.theboomer.dev/docs - AI Tasker — plans, limits and operations catalog, live
https://api.ai-tasker.theboomer.dev/api/plans - AI Tasker — machine-readable catalog of the 40 tools
https://ai-tasker.theboomer.dev/llms-full.txt - AI Tasker — MCP server manifest
https://ai-tasker.theboomer.dev/server.json
Figures verified against the live API and each product’s official page on 21 October 2026. Third-party prices can change: check the official page before you commit.
Related guides
- All guides — How to choose a task manager, MCP and agent lifecycle tooling for AI agents.
- Agent credits and budget: how it is billed — How AI Tasker measures and bills agent usage: the 2 € billable unit, operations, credit packs and auto-refill. No surprises.
- Per-workspace permissions: how agents are scoped — How an agent is scoped in AI Tasker: workspace, quotas, revocation and lifecycle. What scopes limit today and what they do not.
- MCP for task management: guide and comparison — What a task-management MCP server is, which tools each one exposes and how to choose. A table with the real price of every server.
- Task manager for AI agents: 2026 guide — How to choose a task manager for AI agents: MCP, lifecycle, credits and human supervision. A comparison and a checklist.
- Pricing — Real prices: free plan and a 2.00 €/month billable unit.
- MCP integration —
https://mcp.ai-tasker.theboomer.dev/mcp - /llms.txt — Machine-readable contract.
This guide exists to be indexed and cited. It is deliberately not in the menu.