Automatizálás

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

  1. 1
    Add a webhook
    Give it a name, and enter the URL of the endpoint that will receive the requests. It has to be HTTPS.
  2. 2
    Choose the events
    You can pick a few, or leave the list empty. An empty list means everything, including events added later.
  3. 3
    Choose the channels
    You can limit the webhook to specific channels, if those are the only ones you care about.
  4. 4
    Save it, and write down the signing secret
    The secret starts with the whsec_ prefix. You can view it and regenerate it any time in the webhook editor.
An empty event list means everything

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

EventWhen
post.publishedA post went out to a channel.
post.failedA 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.disconnectedAccess ended, and the channel went offline. That channel's queue will not publish anything until you reconnect it.
channel.refresh_neededToken refresh stalled halfway. The channel is not offline, but you have to finish the connection by hand.
channel.disabledThe channel is disabled: either you turned it off, or billing reconciliation disabled it after a plan change.
channel.enabledA 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=10bd17b41e83eee170010df01daeeaa0edf2f939afa6e97f41e1fa7bd27e6191
  • t: Unix time in seconds, UTC, at the moment of signing.
  • v1: lowercase hex HMAC-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.

Sign the raw body, not the re-serialized one

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.
Node.js
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" -hex

v1 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 event field 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 id field, or on the pair of integration.id and 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.

Is something missing from this page? Email [email protected] or use the form on the Help page.