Sennithsennith.
Developer

Webhooks

Have Sennith POST a signed event to your own endpoint whenever an update goes out.

Outgoing webhooks let Sennith call your system whenever something happens in a workspace. Every time an update is posted to one of your projects, Sennith sends a signed POST request to a URL you choose — so you can forward it to Slack, a database, an automation tool, or anything else.

Info

This page is about outgoing webhooks — Sennith notifying you. If instead you want an external service (GitHub, Vercel, Trello and friends) to notify Sennith, that's a trigger — see Connections & integrations.

What gets sent, and when

Right now Sennith emits one real event:

EventWhen it fires
update.createdAn update is posted to a project the webhook is watching.
pingA test event you send yourself from the webhook's settings, to confirm your endpoint is reachable.

Each webhook you create can be narrowed down so you only receive what you care about:

  • Specific projects — or all of them.
  • Only important updates — skip routine ones.
  • Include customer details — attach the recipient's name and email to the payload, or leave them out.
Warning

Your endpoint URL must be served over HTTPS. Plain http:// URLs are rejected.

Creating a webhook

Add the endpoint

In your workspace settings, add a webhook: give it a name, paste your HTTPS URL, and choose which projects and updates it should receive.

Copy the signing secret

When the webhook is created, Sennith generates a signing secret. Keep it safe — you'll use it to verify that incoming calls really came from Sennith. You can regenerate it at any time (which invalidates the old one).

Send a test

Use the Test button to fire a ping at your endpoint and confirm it responds. Sennith shows you the HTTP status it got back.

You can also create and list webhooks through the API with a workspace key holding the outgoing_webhooks scope (GET/POST /api/v1/outgoing_webhooks). The signing secret is only ever shown in the web app, never returned by the API.

The payload

Sennith sends Content-Type: application/json. A update.created body looks like this:

{
  "event": "update.created",
  "delivery_id": 812,
  "created_at": "2026-07-12T10:00:00Z",
  "update": {
    "id": 4021,
    "title": "Staging is live",
    "message": "Preview it at the link below.",
    "content": null,
    "type": "manual",
    "is_important": true,
    "created_at": "2026-07-12T10:00:00Z",
    "public_link": "https://sennith.com/p/a1b2c3"
  },
  "project": { "id": 42, "name": "Website relaunch", "public_id": "a1b2c3" },
  "customer": { "name": "Acme Inc.", "email": "hi@acme.example" }
}

The customer block is only present if you enabled Include customer details for that webhook. This payload shape is a stable contract — you can build against it.

Verifying the signature

Every delivery carries three headers so you can prove it came from Sennith and hasn't been tampered with:

HeaderContents
X-Sennith-EventThe event type, e.g. update.created
X-Sennith-TimestampUnix timestamp (seconds) when the request was signed
X-Sennith-Signaturesha256=<hex> — an HMAC of the request

Requests also carry User-Agent: Sennith-Webhooks/1.

To verify: compute an HMAC-SHA256 over the string <timestamp>.<raw request body> using your webhook's signing secret, prefix it with sha256=, and compare it — with a constant-time comparison — against the X-Sennith-Signature header. Use the raw request body exactly as received, before any JSON parsing.

import { createHmac, timingSafeEqual } from 'crypto'

function isFromSennith(rawBody, headers, secret) {
  const timestamp = headers['x-sennith-timestamp']
  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')
  const got = headers['x-sennith-signature'] ?? ''
  return got.length === expected.length &&
    timingSafeEqual(Buffer.from(got), Buffer.from(expected))
}
Info

Also check that X-Sennith-Timestamp is recent (for example, within a few minutes) to guard against replayed requests.

Responses, retries, and delivery history

Your endpoint should answer quickly — Sennith waits up to 5 seconds — and return any 2xx status to acknowledge receipt. Anything else (an error status, a timeout, an unreachable host) counts as a failure and is retried.

Failed deliveries are retried up to five times with growing gaps:

1 min → 5 min → 30 min → 2 hours → 6 hours

After six total attempts without success, the delivery is marked dead and no longer retried. Because retries are possible, your endpoint should be idempotent — use the delivery_id (stable across retries of the same event) to ignore duplicates.

Every attempt is recorded, so you can review each webhook's recent deliveries — status, response code, and any error — in its settings.

Where to go next

Was this helpful?

On this page