Webhooks
Webhooks allow you to build custom integrations that react automatically to platform events on your account. When an event occurs, the platform dispatches a signed HTTP POST request containing a JSON payload directly to your configured endpoint.
Configuring webhooks
Section titled “Configuring webhooks”Everything is on your Webhooks page (/webhooks, on the console sidebar rail), and it takes two steps:
- Add an endpoint under Endpoints.
- Switch on the events you want under Events.
Every kind starts off and receives nothing until you turn it on. The switches govern this page’s destinations: your endpoints and your Slack or Discord integrations.
- Secret allocation: Each webhook endpoint receives a unique signing secret prefixed with
whsec_when created. The secret is shown only once and is used to cryptographically verify that payloads originate from the platform. - Protocol constraint: Webhook URLs must use HTTPS.
- SSRF prevention: To prevent Server-Side Request Forgery (SSRF), the platform prohibits webhook URLs targeting private or loopback IP ranges (
localhost,127.0.0.1, RFC 1918 subnets like10.0.0.0/8,172.16.0.0/12,192.168.0.0/16, or link-local169.254.169.254). - Scope: Endpoints and event switches both belong to the account. Only the owner registers or removes one.
Event catalog
Section titled “Event catalog”Every delivery carries the same JSON shape — the event kind plus the human-readable notification it mirrors (this example is representative):
{ "id": "0191e6c2-6a4e-7d0b-9c3a-2f1b0e7d8a55", "kind": "member_added", "title": "You joined @alex", "body": "@alex added you to their account as editor.", "metadata": {}}id is the event’s, and it is the same on every delivery of that event. We deliver at least once: a request that reached you but whose acknowledgement was lost is sent again with a fresh signature, so deduplicate on id (or on the Tormoni-Delivery-Id header, which names the delivery row and is likewise stable across its retries).
Event kinds
Section titled “Event kinds”This table is the whole list of what a webhook receives today:
| Event Kind | Meaning |
|---|---|
member_added |
Somebody added you to their account. |
subscription_payment_failed |
A subscription payment did not go through. |
Every kind is off until you switch it on, per kind, on the Webhooks page. Nothing is sent to an endpoint you have not opted in for.
A push that only repeats a run already stored is a retry, not a new run, and raises no event.
Signature verification
Section titled “Signature verification”Every webhook request includes a Tormoni-Signature header in the format t=<unix_timestamp>,v1=<hex_hmac>. The signature is a SHA-256 HMAC of the timestamp and JSON body (t.body). A retried delivery carries a new timestamp and signature over the same body.
Verification steps (Node.js example)
Section titled “Verification steps (Node.js example)”const crypto = require('crypto');
function verifyWebhook(secret, header, rawBody) { const parts = header.split(','); const t = parts.find(p => p.startsWith('t=')).split('=')[1]; const v1 = parts.find(p => p.startsWith('v1=')).split('=')[1];
if (!t || !v1) throw new Error('Invalid signature format');
// Prevent replay attacks (5 minute tolerance) const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(t, 10)) > 300) { throw new Error('Signature timestamp expired'); }
const signatureBase = `${t}.${rawBody}`; const expectedSignature = crypto .createHmac('sha256', secret) .update(signatureBase) .digest('hex');
return crypto.timingSafeEqual( Buffer.from(v1, 'hex'), Buffer.from(expectedSignature, 'hex') );}