Webhookok
A Posty szól a rendszerednek, ha egy bejegyzés kiment vagy elbukott. Események, a törzs alakja és az aláírás ellenőrzése.
What it's for
A webhook is the other way around: you don't poll Posty, we call your system when something happens. A post went out, a publication failed, a channel disconnected.
The feature is available on the Pro plan and above. In the left-hand menu, go to Automations, then open the Webhooks tab. Your plan determines how many webhooks you can have.
Creating a webhook
- 1Add a webhookGive it a name, and enter the URL of the endpoint that will receive the requests. It has to be HTTPS.
- 2Choose the eventsYou can pick a few, or leave the list empty. An empty list means everything, including events added later.
- 3Choose the channelsYou can limit the webhook to specific channels, if those are the only ones you care about.
- 4Save it, and write down the signing secretThe secret starts with the
whsec_prefix. You can view it and regenerate it any time in the webhook editor.
If you don't pick any events, you get every event, including ones we add later. If you pick six, you get exactly those six, and a seventh event added later will not be delivered to this webhook.
The events
| Event | When |
|---|---|
post.published | A post went out to a channel. |
post.failed | A post entered an error state: the platform rejected it, the token became invalid, the channel was offline or disabled, or the plan limit did not allow it. |
channel.disconnected | Access ended, and the channel went offline. That channel's queue will not publish anything until you reconnect it. |
channel.refresh_needed | Token refresh stalled halfway. The channel is not offline, but you have to finish the connection by hand. |
channel.disabled | The channel is disabled: either you turned it off, or billing reconciliation disabled it after a plan change. |
channel.enabled | A suspended channel was turned back on. |
There is no channel.connected event: connecting is a multi-step process, and the intermediate states are not worth reporting.
Every delivery also carries an X-Posty-Event header, but branch on the event field in the body. The header is not in the signature. The field is.
The request body
The body is always a JSON array, for every event. Don't assume the length is always one.
post.published
[
{
"id": "cm6tcts4f0005qcwit25cis26",
"content": "Ez az első bejegyzés Instagramra",
"publishDate": "2026-09-15T09:00:00.000Z",
"releaseURL": "https://instagram.com/p/...",
"state": "PUBLISHED",
"integration": {
"id": "cm6s4uyou0001i2r47pxix6z1",
"name": "Posty",
"providerIdentifier": "instagram",
"picture": "https://uploads.posty.hu/...jpeg",
"type": "social"
},
"event": "post.published"
}
]publishDate is UTC, like every timestamp in Posty.
post.failed
The same object, plus an error field. state is ERROR, and releaseURL is null, because nothing was published. error is the platform's own sentence, if there is one, otherwise null.
channel events
[
{
"event": "channel.disconnected",
"reason": "token_revoked",
"detail": "— access was removed in the account's Meta settings",
"integration": {
"id": "cm6s4uyou0001i2r47pxix6z1",
"name": "Posty",
"providerIdentifier": "instagram",
"picture": "https://uploads.posty.hu/...jpeg",
"type": "social"
}
}
]reason is a closed set: token_revoked, refresh_failed, manual, plan_limit. Branch on that. detail is free text in English, or null: a person reads it, not your code.
Verifying the signature
Every delivery is signed, in the X-Posty-Signature header:
X-Posty-Signature: t=1770000000,v1=10bd17b41e83eee170010df01daeeaa0edf2f939afa6e97f41e1fa7bd27e6191t: Unix time in seconds, UTC, at the moment of signing.v1: lowercase hexHMAC-SHA256(secret, "<t>.<raw body>").- The key is the webhook's own signing secret, with the
whsec_prefix.
This is deliberately the same shape as Stripe's signature, so existing Stripe verification libraries work with it: you only need to change the header name and the secret.
JSON.stringify is not canonical: key order, unicode escaping and number formatting differ by implementation. A receiving system that parses the body first, then turns it back into a string, computes a different digest and rejects a perfectly good delivery. Read the body raw, before you process it.
Verification, step by step
- Read the raw body and the X-Posty-Signature header.
- Split the header on commas, then each part on the first equals sign: that gives you t and v1.
- Reject it if the difference between t and the current time is larger than the tolerance. We use a 300-second tolerance. That is the replay protection.
- Compute HMAC-SHA256(secret, t + "." + rawBody) as lowercase hex.
- Compare it with v1 using a constant-time comparison.
const crypto = require('crypto');
function verify(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(',').map((p) => {
const at = p.indexOf('=');
return [p.slice(0, at).trim(), p.slice(at + 1).trim()];
})
);
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return (
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}To check a captured delivery by hand, the same thing from the shell:
printf '%s' "$T.$RAW_BODY" | openssl dgst -sha256 -hmac "$SECRET" -hexv1 is versioned on purpose: if the scheme ever changes, deliveries will carry both the old and the new element for a while. Match the element name you understand, and ignore the rest. Don't assume the header has exactly two parts.
What the receiving system needs to know
- Branch on the
eventfield in the body before you read anything else, and ignore events you don't know: there will be more of them. - Key on the post's
idfield, or on the pair ofintegration.idand the event, and treat a repeated delivery as idempotent. - Expect one delivery per channel, not one per post group, and don't assume the order.
- If the signature is wrong, reject the request and log it. That is the only way a forged POST becomes visible.
If you need to query rather than receive events, the Notifications endpoint returns the same events as a list.