---
title: "Create a webhook subscription"
method: POST
path: "/projects/{id}/webhooks"
---

# Create a webhook subscription

`POST /projects/{id}/webhooks`

## Request

- Base URL: `https://craaft.io/api/v1`
- URL: `https://craaft.io/api/v1/projects/{id}/webhooks`
- Auth: HTTP bearer

Board admins on Pro boards only. Registers a new endpoint and
subscribes this board to events. The response carries the signing
secret (as do later reads). The URL must be absolute http(s); a
literal private, loopback or link-local IP is rejected, and delivery
re-checks the resolved address so DNS can't route around it.

## Request body

- object
  - `url` string, uri, required
  - `description` string
  - `format` 'craaft' | 'slack' | 'discord': Delivery format (omitted or empty = craaft). 'craaft' sends a signed JSON envelope; 'slack' and 'discord' POST message JSON to webhook URLs registered with those services (unsigned). Slack and Discord messages are grouped per card: changes are sent as one message once the card has been quiet for a minute (at most 5 minutes after the first change). A bulk request is one message for the whole request; a column rebalance sends none.
  - `events` string[]: Event filter; empty=subscribe to all events

## Response `201`

Created

- WebhookSubscription
  - `id` string, uuid, required: Subscription id (the handle for update/delete)
  - `endpointId` string, uuid, required: Endpoint id at the workspace level
  - `url` string, uri, required: The target URL for deliveries
  - `secret` string, required: HMAC signing secret for 'craaft' deliveries (X-Craaft-Signature). Returned on every read to board admins. Slack/Discord deliveries are unsigned and ignore it.
  - `description` string, required: Optional label
  - `format` 'craaft' | 'slack' | 'discord', required: Delivery format (craaft=signed JSON envelope; slack/discord=message POST)
  - `events` string[], required: Event filter (empty=all events; non-empty=only listed events)
  - `active` boolean, required
  - `createdAt` string, date-time, required
  - `recentDeliveries` WebhookDelivery[]: Last few delivery attempts (most recent first), capped at 10
    - `event` string, required: The broadcast event name (e.g. 'card.created')
    - `status` 'delivered' | 'failed', required: Delivery outcome
    - `statusCode` integer, required: HTTP status code from the webhook URL
    - `attempts` integer, required: Number of delivery attempts
    - `error` string, required: Error message when status is failed; empty string on success (never null)
    - `createdAt` string, date-time, required

## Other responses

- `400`: Malformed body or SSRF-rejected URL
- `401`: Missing or invalid token
- `402`: Free plan
- `403`: Not a board admin
- `404`: Resource doesn't exist or caller isn't a member of its workspace

---

[API](https://skmtc.dev/craaft/apis/craaft-api.md) · [All operations](https://skmtc.dev/craaft/apis/craaft-api/llms.txt) · [OpenAPI document](https://skmtc.dev/craaft/apis/craaft-api/revisions/3a1e9d30d7dc?raw)
