---
title: "Create a webhook"
method: POST
path: "/api/webhooks"
tags: ["Webhooks"]
---

# Create a webhook

`POST /api/webhooks`

## Request

- Base URL: `https://www.flyn.to`
- URL: `https://www.flyn.to/api/webhooks`
- Auth: HTTP bearer

Subscribes an endpoint to events and returns the signing secret in full. Store it now: API-key callers only ever see a masked secret afterwards. The webhook is tagged with the caller's current workspace.

**Auth:** API key (`Bearer flyn_sk_live_...`), or the signed-in dashboard session cookie when no API key header is sent. MCP OAuth access tokens are not accepted here and get 401.

**Plan:** Pro, Lifetime or Team (a member of a paying owner's workspace has the effective plan `team`) only; Free gets 403 `UPGRADE_REQUIRED`. At most 10 webhooks per user (any status, any workspace).

**Rate limit:** 10 requests per 60 s per client IP, fixed window, in memory per server instance, checked before authentication.

An invalid JSON body, a non-string url or a non-array events returns 500.

## Request body

- WebhookCreateRequest
  - `url` string, required: Endpoint that receives deliveries. Trimmed; http or https; must not resolve to a private address. A non-string value returns 500.
  - `events` WebhookEventName[], required: Events to subscribe to. Unknown names are dropped silently; 400 only when none remain. A non-array value returns 500.

## Response `201`

Created. `secret` is shown in full.

- WebhookEnvelope
  - `webhook` Webhook, required
    - `id` string, uuid, required
    - `url` string, required: Delivery endpoint.
    - `events` string[], required: Subscribed event names, as stored.
    - `secret` string, nullable, required: Signing secret, `whsec_` + 48 hex. Always in full in the create response. Afterwards API-key callers see only a masked prefix (first 8 characters); the creator's own dashboard session sees it in full.
    - `status` 'active' | 'inactive', required: A webhook becomes inactive after 10 counted delivery failures in a row.
    - `createdAt` integer, required: Unix time in milliseconds.
    - `lastTriggeredAt` integer, nullable: Unix time in milliseconds of the last successful delivery. Absent from the create response.
    - `failureCount` integer, required: Counted failures since the last success (non-2xx answers, and skips of a URL whose host resolves to a private address or fails DNS resolution; timeouts and connection errors are not counted).

## Other responses

- `400`: Messages: "Webhook URL is required", "Only HTTP/HTTPS URLs allowed", "Webhook URL must be a public address", "Invalid URL", "Select at least one event", "Maximum 10 webhooks allowed".
- `401`: No valid API key and no dashboard session. Messages: "Invalid or revoked API key", "API key has expired", "Unauthorized. Sign in to the dashboard, or pass a Bearer API key in the Authorization header.", or "Unauthorized" when the session lookup itself fails. An MCP OAuth token lands here too.
- `403`: Free plan (code `UPGRADE_REQUIRED`) or a suspended account.
- `429`: More than 10 requests in the window. Counted per client IP. Retry-After gives the seconds until the window resets.
- `500`: Invalid JSON, a non-string url, a non-array events, or a database error.

---

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