Events
Receiving webhooks
Events, payloads and verifying signatures.
SocialHelper can call a URL of yours when a post finishes or an account needs attention, so you don’t have to poll. Add the URL under Webhooks in the dashboard; you’ll get a signing secret starting with whsec_.
The URL must be publicly reachable, and in production it must use HTTPS. Use Send test event to try it.
Events
-
post.publishedevent - Every account in the post was published.
-
post.partially_publishedevent - Some accounts were published and some failed.
-
post.failedevent - No account was published.
-
post.cancelledevent - Every account in the post was cancelled.
-
account.needs_reconnectevent - An account lost access and must be reconnected in the dashboard.
-
pingevent - The test event sent from the dashboard’s Webhooks page.
Post events are sent once, when the post first reaches its final status.
Payload
Each delivery is a POST with a JSON body. For post events, data is the post with its targets, exactly as the API returns it; for account events, it’s the account.
{
"id": "evt_01ja8z…",
"type": "post.published",
"created_at": "2026-10-10T18:00:00Z",
"data": {
"id": "01JA8Z…",
"object": "post",
"status": "published",
"targets": [
"…"
]
}
}
-
SocialHelper-Eventheader - The event type, same as type in the body.
-
SocialHelper-Deliveryheader - The event ID. It stays the same on retries, so use it to ignore duplicates.
-
SocialHelper-Signatureheader - t=<unix time>,v1=<signature>
Verify the signature
The signature is an HMAC-SHA256, in hex, of the timestamp, a full stop and the raw body, using your signing secret: v1 = HMAC_SHA256(secret, t + "." + body). Compare it in constant time, and reject old timestamps to stop replays (five minutes is a sensible window).
import crypto from "node:crypto";
// Use the raw request body: re-encoded JSON won't match the signature.
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const ok = expected.length === parts.v1?.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return ok && fresh;
}
<?php
// Use the raw request body: re-encoded JSON won't match the signature.
function verify(string $rawBody, string $header, string $secret): bool
{
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'].'.'.$rawBody, $secret);
$fresh = abs(time() - (int) $parts['t']) < 300;
return hash_equals($expected, $parts['v1'] ?? '') && $fresh;
}
Delivery and retries
- Reply with any
2xxstatus within 15 seconds. Anything else, or a timeout, counts as a failure. - Failed deliveries are retried up to 7 more times, after 10 seconds, 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour and 4 hours: about six hours in all.
- Each attempt is signed again with a fresh timestamp.
- The dashboard shows when the endpoint last succeeded or failed, and the last error.