0.x — pre-release, no compatibility promise yet.What this means
Webhooks
What this is: TasksMate POSTs a signed JSON event to your HTTPS endpoint whenever something you subscribed to changes. When you need it: to react to changes instead of polling the API.
Verify every delivery before you trust it — over the raw body, before parsing:
from tasksmate import webhooks
def receive(headers, raw_body: bytes): # SECRET: the whsec_… shown once when you created the webhook if not webhooks.verify(SECRET, headers, raw_body): return 400 event = webhooks.parse(raw_body) # .id .type_ .org_id .data return 200 # dedupe on event.idimport base64, hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool: msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"] if abs(time.time() - int(ts)) > 300: return False key = base64.b64decode(secret.removeprefix("whsec_")) # decoded bytes signed = f"{msg_id}.{ts}.".encode() + raw_body digest = hmac.new(key, signed, hashlib.sha256).digest() expected = "v1," + base64.b64encode(digest).decode() sigs = headers["webhook-signature"].split(" ") return any(hmac.compare_digest(expected, s) for s in sigs)const crypto = require("crypto");
function verify(secret, headers, rawBody) { const id = headers["webhook-id"], ts = headers["webhook-timestamp"]; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); // decoded bytes const expected = "v1," + crypto.createHmac("sha256", key) .update(`${id}.${ts}.`).update(rawBody).digest("base64"); return headers["webhook-signature"].split(" ").some( (s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)), );}How a delivery works
Section titled “How a delivery works”- You register an endpoint you already run — in Developers → Webhooks or with
POST /v1/webhooks— and pick events and projects. The response shows the signing secret once. - Something changes in your organization: a task moves, a project is renamed.
- TasksMate queues one delivery per active webhook that wants that event.
- TasksMate sends it: a
POSTsigned with your secret, no redirects followed, waiting up to 20 s for your answer. - You verify and answer
2xx. Do the real work after answering. - No
2xx? It is retried at +30 s, +2 m, +10 m, +30 m, +2 h, +6 h, +12 h — 8 attempts in about 20 h 43 m, each re-signed.410 Gonedisables the webhook;429withRetry-Afteris honoured up to 6 h; 10 failures in a row disable it until you re-enable it.
Two keys, two jobs
Section titled “Two keys, two jobs”| Key | Looks like | What it does |
|---|---|---|
| Webhook secret | whsec_… |
Signs every delivery to you. You verify with it; it is never sent in a delivery. |
| Access token | tm_live_… |
Authenticates your calls to the API — e.g. fetching the full task an event names. |
Don’t mix them up: a delivery carries a signature, never a token. TasksMate stores your secret encrypted and shows it only when a webhook is created or its secret rotated.
What arrives
Section titled “What arrives”| Header | Example | What it is |
|---|---|---|
webhook-id | WD000001 | The delivery id — equal to the body's id. Dedupe on it (delivery is at-least-once). |
webhook-timestamp | 1790380800 | Unix seconds when this attempt was signed. Refuse one more than 5 minutes from now (a replay). |
webhook-signature | v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= | Space-separated v1,<base64> HMAC-SHA256 signatures of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the secret after whsec_. Two during a rotation's 24 h grace. |
X-TasksMate-Event | task.updated | The event type (the body's type), for routing before parsing. |
User-Agent | TasksMate-Webhooks/1 | Identifies TasksMate's sender. |
The body is a WebhookEvent. data.before / data.after carry what changed — never the whole resource. Fetch
that with your token.
{ "id": "WD000001", "type": "task.updated", "api_version": "2026-09-25", "created_at": "2026-09-25T12:00:00Z", "org_id": "O0020", "project_id": "P30104", "actor": { "kind": "user", "id": "406670f1-c819-4d27-9552-1747c551cf5c", "username": "ada" }, "data": { "resource_type": "task", "resource_id": "T869658", "before": { "status": "not_started" }, "after": { "status": "in_progress" } }, "request_id": "9b2f1c1e-8c1a-4a53-9f9e-0f5f1f2d7c11"}Rotating the secret
Section titled “Rotating the secret”Rotate secret reveals a new secret once. For 24 h every delivery carries two signatures (old and new, space-separated), so split the header and accept either while you switch.
Rules of thumb
Section titled “Rules of thumb”- Hash the raw bytes; re-serialized JSON will not match.
- Deliveries are at-least-once and may arrive out of order: dedupe on
id, order bycreated_at. - Answer fast; queue the work.
Every event you can subscribe to: Events.