---
title: "Create a webhook"
method: POST
path: "/workspaces/{workspaceId}/webhooks"
tags: ["Webhooks"]
---

# Create a webhook

`POST /workspaces/{workspaceId}/webhooks`

## Request

- Base URL: `https://tasg.app/api/v1`
- URL: `https://tasg.app/api/v1/workspaces/{workspaceId}/webhooks`
- Auth: HTTP bearer

Workspace owner only, max 10 per workspace. Delivery starts from the CURRENT stream position (future events only). The signing `secret` (`whsec_…`) is returned ONCE in this response and never again; store it.

**Verifying deliveries.** Each POST carries `X-Webhook-Timestamp` (unix seconds) and `X-Webhook-Signature: v1=<hex>`, where the hex is HMAC-SHA256 of `"<timestamp>.<raw body>"` keyed with your secret. Recompute it over the RAW body, compare constant-time, and reject timestamps older than ~5 minutes. Delivery is at-least-once and ordered per webhook; dedupe on `event.id`. Respond 2xx quickly; anything else is retried with backoff (30s → 2m → 10m → 30m → hourly), and 20 consecutive failures disable the webhook.

## Path parameters

- `workspaceId` string, uuid, required

## Request body

- object
  - `url` string, uri, required: Public https(s) endpoint. Private/internal addresses are refused, and redirects are not followed.
  - `description` string
  - `event_types` EventType[]: Omit (or send []) to receive every type.

## Response `201`

Created. Includes the secret; this is the only time.

- object
  - `webhook` object, required
    - `id` string, uuid, required
    - `url` string, uri, required
    - `description` string, nullable
    - `event_types` EventType[], required: Empty array = subscribed to every type.
    - `active` boolean, required
    - `cursor` string, required: Stream position of the last delivered event.
    - `failure_count` integer, required: Consecutive failed deliveries; resets to 0 on success.
    - `next_attempt_at` string, date-time, nullable: When the next retry is due while backing off.
    - `disabled_reason` string, nullable: Set when the subscription was auto-disabled (e.g. 20 consecutive failures).
    - `last_delivery_at` string, date-time, nullable
    - `created_at` string, date-time, required
    - `secret` string, required: Signing secret, shown once.

## Other responses

- `400`: Validation failed: the body or query did not match the schema, or a business rule was violated. `error` says which.
- `401`: Missing, invalid, expired or revoked credentials.
- `403`: Authenticated, but not allowed: not a member, insufficient project role or the API token lacks the required scope.
- `429`: Rate limited. Back off and retry after the window in the `RateLimit-*` headers.

---

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