Skip to content
SocialMate

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

HeaderValue
X-SocialMate-EventThe event name, e.g. message.received.
X-SocialMate-TimestampUnix-ms send time — used in the signature and the replay window.
X-SocialMate-Signaturesha256=<hex> when the endpoint has a secret (see below).
User-AgentSocialMate-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 2xx quickly and do your work asynchronously — slow receivers risk the timeout. De-duplicate on messageId/itemId since 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's media.apiPath is ready to fetch the moment it arrives. Text-only events fire immediately. See the media slice.

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

GET /v1/webhooks  ·  /v1/webhooks/{id}

scope: admin

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 } ] }
POST /v1/webhooks

scope: admin

Create 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 } }

Using n8n? The native SocialMate n8n node registers this webhook for you — no manual endpoint needed.

PATCH /v1/webhooks/{id}

scope: admin

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 } }
DELETE /v1/webhooks/{id}

scope: admin

Remove an endpoint.

curl -X DELETE http://localhost:3456/v1/webhooks/wh_1 \
  -H "x-api-key: ADMIN_KEY"
{ "data": { "ok": true } }
POST /v1/webhooks/{id}/test

scope: admin

Fire 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 } }
GET /v1/webhooks/{id}/deliveries

scope: admin

Recent 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 } ] }
Free forever · no card Download free