# Webhooks

> Register a signed outbound webhook for one or more Taskade events

- Source: https://www.taskade.com/docs/actions/webhooks-post
- Section: Action API
- Endpoint: `POST /webhooks` (API v2)
- Index of all pages: https://www.taskade.com/docs/llms.txt

## POST `/webhooks`

Register a signed outbound webhook for one or more Taskade events.

Each delivery is a POST with a JSON body and three signature headers:

- `X-Taskade-Signature`: `sha256=<hex>`, the HMAC-SHA256 of the raw body, keyed with your `secret`.
- `X-Taskade-Timestamp`: the send time, in unix seconds.
- `X-Taskade-Signature-V2`: `sha256=<hex>`, the HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with your `secret`.

To verify V2:

1. Read the raw request body before you parse the JSON.
2. Compute the HMAC-SHA256 of the `X-Taskade-Timestamp` value, a period (`.`) and the raw body. Use your `secret` as the key.
3. Compare `sha256=<your hex>` with `X-Taskade-Signature-V2` in constant time.
4. Reject the request if the timestamp is more than 5 minutes away from your clock.

The timestamp is part of the signed data, so a captured delivery fails the check after 5 minutes. A copy that arrives inside those 5 minutes still passes, so make your handler idempotent, or record each delivery that you process and skip a repeat. `X-Taskade-Signature` does not change, and receivers that verify it keep working.

Base URL: `https://www.taskade.com/api/v2`

### Authentication

Send a personal access token or an OAuth 2.0 access token as a bearer token. Create a personal access token at [taskade.com/settings/api](https://www.taskade.com/settings/api).

```http
Authorization: Bearer tskdp_your_token_here
```

### Request body

`application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `targetUrl` | `string` | Yes | — |
| `events` | `string[]` | Yes | — |
| `spaceIds` | `string[]` | No | — |

### Example request

```bash
curl -X POST "https://www.taskade.com/api/v2/webhooks" \
  -H "Authorization: Bearer $TASKADE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"targetUrl":"<targetUrl>","events":["task.due"]}'
```

### Responses

#### `200` — Default response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ok` | `boolean` | Yes | — |
| `webhook` | `object` | Yes | — |
| `webhook.id` | `string` | Yes | Endpoint id - the target URL (use URL-encoded as the :id path param). |
| `webhook.url` | `string` | Yes | — |
| `webhook.events` | `string[]` | Yes | — |
| `webhook.spaceIds` | `string[]` | Yes | Spaces this webhook is scoped to; empty = all spaces. |
| `webhook.createdAt` | `string` | Yes | — |
| `secret` | `string` | Yes | HMAC signing secret - shown once. Store it now. |

A nested field marked Yes is required whenever its parent object is present.

### Errors

Errors return an `ok: false` envelope. Rate limiting returns `429` with `x-rate-limit-limit`, `x-rate-limit-remaining` and `x-rate-limit-reset` headers. `x-rate-limit-reset` is the number of seconds until the window reopens. `Retry-After` is not sent, so schedule retries from `x-rate-limit-reset`; retrying sooner will fail again.

```json
{ "ok": false, "message": "Unauthorized", "code": "UNAUTHORIZED" }
```

Rate limits for this version: enforced per endpoint; ceilings are not published and can change without notice. A `429` response carries `x-rate-limit-limit`, `x-rate-limit-remaining`, and `x-rate-limit-reset` (seconds until the window reopens). Back off from those headers rather than hardcoding numbers. If you need sustained throughput, prefer batching (operations like `/createTask` accept arrays) over tightening polling loops. See [Rate limits](https://www.taskade.com/docs/getting-started/action-api-guide#rate-limits).
