# Webhooks

> Receive a signed POST when something changes in TasksMate — register, verify, answer 2xx.

**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:

**Python SDK**


```python
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.id
```

**Python (stdlib)**


```python
import 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)
```

**Node**


```js
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

One delivery: signed, verified, acknowledged — or retried

1.  **You register an endpoint** you already run — in [Developers → Webhooks](https://tasksmate.indrasol.com/developers?dev=webhooks) or with [`POST /v1/webhooks`](https://developers.tasksmate.indrasol.com/reference/operations/webhookscreate/) — and pick [events](https://developers.tasksmate.indrasol.com/guides/webhooks/events/) and projects. The response shows the signing secret **once**.
2.  **Something changes** in your organization: a task moves, a project is renamed.
3.  **TasksMate queues one delivery** per active webhook that wants that event.
4.  **TasksMate sends it:** a `POST` signed with your secret, no redirects followed, waiting up to 20 s for your answer.
5.  **You verify and answer `2xx`.** Do the real work after answering.
6.  **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 Gone` disables the webhook; `429` with `Retry-After` is honoured up to 6 h; 10 failures in a row disable it until you re-enable it.

## 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

| 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.

**task.updated**

```json
{
  "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

**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

-   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 by `created_at`.
-   Answer fast; queue the work.

Every event you can subscribe to: [Events](https://developers.tasksmate.indrasol.com/guides/webhooks/events/).

---
Source: https://developers.tasksmate.indrasol.com/guides/webhooks/
