Setup
One webhook endpoint per partner receives the events of every workspace. Each delivery says which workspace and which external_id it belongs to, so a single route in your app fans out to the right customer.
Endpoints
Section titled “Endpoints”| Method | Path | Body | Returns |
|---|---|---|---|
| GET | /v1/webhook |
{ url, events, secret_prefix, created } or null when none is set |
|
| PUT | /v1/webhook |
{ url, events?: ["*"] } |
{ url, events, secret }. The secret is shown once |
| DELETE | /v1/webhook |
204 |
|
| POST | /v1/webhook/test |
Sends a ping event to the URL right away and returns that event (same shape as an item of GET /v1/events, with delivery.status, delivery.attempts and delivery.last_status) |
|
| GET | /v1/events |
query: workspace, type, since, page, limit |
List of events, for polling |
Webhook endpoints need a partner key. A workspace key cannot read or change them.
Register
Section titled “Register”curl -X PUT https://social.missless.tel/v1/webhook \ -H "Authorization: Bearer $MISSLESS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/webhooks/missless", "events": ["*"] }'Response:
{ "url": "https://app.example.com/webhooks/missless", "events": ["*"], "secret": "7d0b7c8f5a1e4c2b9f6d3a8e1c4b7f0a9d2e5c8b1a4f7d0c"}Store secret in your secret manager as MISSLESS_WEBHOOK_SECRET. It is returned by this call only. GET /v1/webhook shows secret_prefix, the first characters, so you can confirm which secret is live without exposing it.
Requirements for url:
httpsonly. Plain http is rejected withvalidation_erroronfield: "url".- Publicly reachable. Loopback, link-local and private network addresses are refused.
- Answers
2xxwithin a few seconds. Do the work after you respond, not before.
Choose event types
Section titled “Choose event types”events is a list of type names, or ["*"] for everything.
{ "url": "https://app.example.com/webhooks/missless", "events": ["post.published", "post.partial", "post.failed", "account.expired"] }ping is always delivered, whatever the filter. The full list is in Events.
Rotate the secret
Section titled “Rotate the secret”PUT /v1/webhook replaces the whole configuration, secret included. To rotate, call it again with the same url, read the new secret, and deploy it. Deliveries signed with the old secret stop as soon as the call returns, so deploy the new secret first if you cannot tolerate a gap; verify against both for a few minutes.
curl -X POST https://social.missless.tel/v1/webhook/test \ -H "Authorization: Bearer $MISSLESS_API_KEY"Your URL receives a ping:
{ "id": "evt0a2c4e6g8i1k", "type": "ping", "created_at": "2026-08-25T09:00:00Z", "partner": "acme", "workspace": null, "external_id": null, "data": {}}with the same headers as a real event, so you can test signature verification end to end. See Verify signatures.
Remove
Section titled “Remove”DELETE /v1/webhook stops deliveries. Events keep being recorded and stay readable through GET /v1/events, so nothing is lost while you have no endpoint. Register again when you are ready.
A minimal receiver
Section titled “A minimal receiver”The receiver has three jobs: verify, acknowledge, dispatch. In that order.
import express from 'express';import { verifyMissLess } from './verify.mjs';
const app = express();
app.post('/webhooks/missless', express.raw({ type: 'application/json' }), (req, res) => { if (!verifyMissLess(req.body, req.headers, process.env.MISSLESS_WEBHOOK_SECRET)) { return res.status(401).end(); } const event = JSON.parse(req.body.toString('utf8')); res.status(200).end();
queue.push(event); // handle asynchronously, de-duplicate on event.id});verifyMissLess is on the next page. The body must be read raw, before any JSON middleware touches it, because the signature covers the exact bytes.