SocialMate Local API
Webhooks
Subscribe to real-time events: payload shape, HMAC signatures, retries, and delivery logs.
Webhooks push events from SocialMate to your own URL the moment they happen — inbound
messages, send results, sync and tunnel state, queue lifecycle, and more. No polling. Manage
them from the app's API page or over HTTP (the CRUD routes need the admin scope).
Payload shape
Every delivery is a JSON POST with a stable wrapper. Branch on
version for forward-compatibility; the event-specific fields live under
data (catalogued on Webhook events).
{
"version": 1,
"event": "message.received",
"timestamp": "2026-06-03T12:00:00.000Z",
"tunnelUrl": "https://your-name.trycloudflare.com",
"data": { /* event-specific */ }
}
Delivery headers
| Header | Value |
|---|---|
X-SocialMate-Event | The event name, e.g. message.received. |
X-SocialMate-Timestamp | Unix-ms send time — used in the signature and the replay window. |
X-SocialMate-Signature | sha256=<hex> when the endpoint has a secret (see below). |
User-Agent | SocialMate-Webhook/1.0 |
Free-tier deliveries also carry an X-Powered-By: SocialMate Free (https://socialmate.app)
header; it's absent on Pro.
Verifying the signature
If you set a secret on the endpoint, each delivery is signed with HMAC-SHA256 over
<timestamp>.<raw-body> (Stripe-style). Recompute it with your secret and
compare in constant time; also reject timestamps outside a short window (≈5 min) to defeat
replays.
// Node (express raw body)
import crypto from 'node:crypto';
function verify(req, secret) {
const ts = req.header('X-SocialMate-Timestamp');
const sig = req.header('X-SocialMate-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${ts}.${req.rawBody}`)
.digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
const fresh = Math.abs(Date.now() - Number(ts)) < 5 * 60 * 1000;
return ok && fresh;
}
Retries & reliability
- Each delivery is attempted up to 3 times with exponential backoff — 5s → 25s → 125s — and a 10 second per-attempt timeout.
- Every attempt is written to a delivery log you can read back (below).
- An endpoint that fails every retry repeatedly is auto-disabled to stop hammering a dead URL; re-enable it once fixed.
- Respond
2xxquickly and do your work asynchronously — slow receivers risk the timeout. De-duplicate onmessageId/itemIdsince a retry can re-deliver. - Deliveries that carry media —
message.received,message.sent,media.discovered— are held until the media download settles (usually a few seconds, with a ~2-minute backstop), so the payload'smedia.apiPathis ready to fetch the moment it arrives. Text-only events fire immediately. See themediaslice.
Tier limits
Free allows 2 active endpoints, the 9 Free events, and
100 deliveries per calendar day (the counter resets at midnight UTC). Pro is
unlimited across all 35 events. The allow-list is enforced when you save the subscription:
creating or updating an endpoint whose events include a Pro-only event returns
402 license_required ("These webhook events require Pro"), and
re-enabling a disabled endpoint counts against the Free 2-endpoint cap (also 402).
Endpoints
/v1/webhooks · /v1/webhooks/{id}List endpoints, or fetch one. The secret is never returned — only hasSecret
tells you whether one is set.
Reads need admin, not read. A webhook row carries
its full target url, and a webhook URL is a capability: anyone holding it can POST
events into whatever it feeds. read means “your WhatsApp data”, never
“how this server is wired” — so listing endpoints is an admin operation.
curl http://localhost:3456/v1/webhooks \
-H "x-api-key: YOUR_KEY"
{ "data": [ { "id": "wh_1", "label": "n8n: orders", "url": "https://n8n.example.com/webhook/abc", "events": ["message.received"], "hasSecret": true, "enabled": true, "accountId": "a1b2c3", "accounts": ["a1b2c3"], "consecutiveFailures": 0, "disabledReason": null, "createdAt": 1718800000000, "updatedAt": 1718800000000 } ] }
/v1/webhooksCreate an endpoint. Body: label + url (http/https, required),
optional events (array), secret, enabled. On Free,
a third endpoint or an events list containing Pro-only events is rejected with
402 license_required.
curl -X POST http://localhost:3456/v1/webhooks \
-H "x-api-key: ADMIN_KEY" -H "Content-Type: application/json" \
-d '{
"label": "n8n inbound",
"url": "https://n8n.example.com/webhook/sm",
"events": ["message.received", "message.sent"],
"secret": "whsec_…"
}'
{ "data": { "id": "wh_1", "label": "n8n inbound", "url": "https://n8n.example.com/webhook/sm", "events": ["message.received", "message.sent"], "hasSecret": true, "enabled": true, "accountId": "a1b2c3", "accounts": ["a1b2c3"], "consecutiveFailures": 0, "disabledReason": null, "createdAt": 1718800000000, "updatedAt": 1718800000000 } }
/v1/webhooks/{id}Update any of label, url, events, secret,
enabled (e.g. re-enable an auto-disabled endpoint). The same Free-tier gates apply
as on create: widening events to Pro-only events returns 402, and
enabled: true on a disabled endpoint counts against the 2-active-endpoint cap.
curl -X PATCH http://localhost:3456/v1/webhooks/wh_1 \
-H "x-api-key: ADMIN_KEY" -H "Content-Type: application/json" \
-d '{ "events": ["message.received", "message.sent"], "enabled": true }'
{ "data": { "id": "wh_1", "label": "n8n inbound", "url": "https://n8n.example.com/webhook/sm", "events": ["message.received", "message.sent"], "hasSecret": true, "enabled": true, "accountId": "a1b2c3", "accounts": ["a1b2c3"], "consecutiveFailures": 0, "disabledReason": null, "createdAt": 1718800000000, "updatedAt": 1718800100000 } }
/v1/webhooks/{id}Remove an endpoint.
curl -X DELETE http://localhost:3456/v1/webhooks/wh_1 \
-H "x-api-key: ADMIN_KEY"
{ "data": { "ok": true } }
/v1/webhooks/{id}/testFire a sample delivery so you can confirm receipt and signature handling. Optional body:
event, sample. This makes a single attempt (not the background retry
ladder) and returns the real result right away: ok is true only when
your endpoint answered 2xx, otherwise statusCode and error
explain why (e.g. an HTTP 403 or an unreachable host).
curl -X POST http://localhost:3456/v1/webhooks/wh_1/test \
-H "x-api-key: ADMIN_KEY" -H "Content-Type: application/json" \
-d '{ "event": "message.received" }'
{ "data": { "ok": true, "statusCode": 200, "error": null } }
/v1/webhooks/{id}/deliveriesRecent delivery attempts for this endpoint (status, HTTP code, attempt #, error).
limit query, max 500.
curl "http://localhost:3456/v1/webhooks/wh_1/deliveries?limit=50" \
-H "x-api-key: YOUR_KEY"
{ "data": [ { "id": "del_1", "endpointId": "wh_1", "event": "message.received", "status": 200, "attempt": 1, "at": 1718800000000 } ] }