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

# Create a webhook

`POST /v1/webhooks`

## Request

- Base URL: `https://api.sonovault.now`
- URL: `https://api.sonovault.now/v1/webhooks`
- Auth: API key in header `x-api-key`

Register an endpoint to receive a signed HTTP POST for every confirmed play on your monitored streams. No polling. Deliveries are settled before they are sent, so you get one event per airing and none for a detection we later discard (see Delivery timing). The response includes the signing `secret` once; store it to verify deliveries. Bundled with stream monitoring (no API credits).

**Verifying deliveries**

- Every POST carries `Sonovault-Signature: t=<unix>,v1=<hex>` where `v1 = HMAC-SHA256(secret, "{t}.{rawBody}")`. Recompute over the raw body and compare; reject if `t` is too old (replay protection).

- Also sent: `Sonovault-Event-Id` (stable across retries, so dedupe on it) and `Sonovault-Event-Type`.

- Return any 2xx to acknowledge. Non-2xx / timeouts are retried with exponential backoff (~10s → 6h, given up after 12 attempts); sustained failures auto-disable the endpoint.

**Delivery timing**

- A `stream.play.started` webhook arrives about 5 minutes after the play began, not the instant it is detected. `started_at` is always the real airing start, so the timeline you build is unaffected.

- The delay is deliberate. Deciding whether a detection is a real airing needs a few minutes of hindsight: a brief blip has to be seen ending, and a momentary mis-detection is only recognisable once the interrupted track resumes. Waiting means one webhook per airing, and none for a detection that never was.

- If you want the lowest possible latency instead, use the live SSE feed (`GET /v1/streams/live`), which pushes the moment a track is detected and may occasionally show a detection that later resolves differently.

**Event payload**

- `stream.play.started` - `{ id, type, created, data: { stream_id, started_at, track } }`. (A track-end event isn't offered yet; there's no reliable end signal.)

- `stream.offline` - `{ id, type, created, data: { stream_id, at, reason } }`. Sent when we detect a stream went down.

- `stream.online` - `{ id, type, created, data: { stream_id, at } }`. Sent when a down stream recovers.

- `track` is the standard public track object (same shape as search results).

## Request body

- object
  - `url` string, required: your HTTPS endpoint; we POST events here.
  - `event_types` string: which events to receive: `stream.play.started`, `stream.offline`, `stream.online`. Defaults to `stream.play.started` only; list the others to subscribe.
  - `description` string: a label shown in your dashboard.

## Response `200`

Successful response

- Webhook: A registered webhook endpoint.
  - `id` string, required: Webhook ID (UUID).
  - `url` string, required: Your HTTPS endpoint, where events are POSTed.
  - `description` string, nullable, required: Your label for the endpoint. `null` when you did not set one.
  - `event_types` string[], required: Which events this endpoint receives.
  - `enabled` boolean, required: `false` once you disable it, or after sustained delivery failures auto-disable it.
  - `secret` string: The signing secret for `Sonovault-Signature`. Returned **only** by `POST /v1/webhooks`, once. Store it then; it is never shown again.
  - `created_at` string, required: When the endpoint was registered, ISO-8601 UTC.
  - `last_delivery_at` string, nullable, required: Last delivery attempt, ISO-8601 UTC. `null` before the first one.

---

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