---
title: Webhooks
description: HTTPS endpoints for link.visit, link.download, and link.play, delivered by Svix.
sidebar:
  label: Webhooks
  order: 7
---

HTTPS endpoints. HTTPS only. No localhost or private IPs. Up to 10 per account.

Events: `link.visit`, `link.download`, `link.play`. Secret is `whsec_…`, shown once on create/rotate. Keep it. Never put it in the JSON body.

## CLI

```bash
linkdash webhooks create --url https://example.com/hooks
linkdash webhooks list
linkdash webhooks get <id>
linkdash webhooks rotate <id>
linkdash webhooks delete <id>
```

`list` and `get` return `secretHint` only (`whsec_…xxxx`).

## HTTP

```bash
curl https://api.linkdash.dev/v1/webhooks \
  -X POST \
  -H "Authorization: Bearer ldash_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks"}'
```

`GET /webhooks`, `POST /webhooks`, `GET /webhooks/:id`, `DELETE /webhooks/:id`, `POST /webhooks/:id/rotate`.

## Payload

```json
{
  "type": "link.visit",
  "timestamp": "2026-08-17T16:00:00.000Z",
  "data": { "id": "deck", "kind": "file", "country": "DE", "referrer": "…" }
}
```

Headers on every delivery (Svix retries reuse the same id):

- `svix-id`
- `svix-timestamp` (unix seconds)
- `svix-signature` (`v1,<base64 hmac>`)

Signed content: `{id}.{timestamp}.{raw body}` HMAC-SHA256. Secret format `whsec_` + base64.

## Verify

Deliveries are sent by [Svix](https://www.svix.com) and follow the [Standard Webhooks](https://www.standardwebhooks.com) spec. Verify with the [`svix`](https://www.npmjs.com/package/svix) package (same as Clerk and Resend).

:::note
Pass the **raw** request body to `verify`. Parsing JSON and stringifying it again will fail the signature.
:::

```bash
npm i svix
```

```ts
import { Webhook, WebhookVerificationError } from "svix";

const wh = new Webhook(process.env.LINKDASH_WEBHOOK_SECRET); // whsec_… shown once

try {
  const event = wh.verify(rawBody, {
    "svix-id": headers["svix-id"],
    "svix-timestamp": headers["svix-timestamp"],
    "svix-signature": headers["svix-signature"],
  });
  // event.type is "link.visit" | "link.download" | "link.play"
} catch (err) {
  if (err instanceof WebhookVerificationError) {
    // 400 — do not process
  }
  throw err;
}
```
