# PostEverywhere Webhooks: Know the Moment a Post Publishes or Fails > Stop polling for publish status. PostEverywhere webhooks push a signed JSON payload the moment a post publishes, a destination fails, or a social account needs reconnecting. Every event, payload, header and retry rule. **Source:** https://posteverywhere.ai/blog/posteverywhere-webhooks **Author:** Jamie Partridge **Published:** 2026-09-12 --- Here is the loop almost every integration starts with. You schedule a post through the API, then you poll `GET /v1/posts/{id}` every few seconds to find out whether it actually went live. It works, it burns your rate limit, and it is still the slowest way to learn that something failed. PostEverywhere webhooks remove the loop. Subscribe once, and PostEverywhere POSTs a signed JSON payload to your endpoint the moment a destination publishes, a post fails, or a social account's token dies. > "Tell me the instant anything fails, and let me stop asking." That is the whole idea. This guide covers every PostEverywhere webhook event, what the payload actually contains, how to verify the signature properly, and exactly how deliveries are retried when your endpoint is down. *Written by Jamie Partridge, Founder.* ## Table of Contents 1. [What PostEverywhere webhooks replace](#what-posteverywhere-webhooks-replace) 2. [Every PostEverywhere webhook event, and which ones actually fire](#every-posteverywhere-webhook-event-and-which-ones-actually-fire) 3. [What a PostEverywhere webhook payload contains](#what-a-posteverywhere-webhook-payload-contains) 4. [Headers PostEverywhere sends with every delivery](#headers-posteverywhere-sends-with-every-delivery) 5. [How to verify a PostEverywhere webhook signature](#how-to-verify-a-posteverywhere-webhook-signature) 6. [How PostEverywhere retries a failed delivery](#how-posteverywhere-retries-a-failed-delivery) 7. [Testing your endpoint before you trust PostEverywhere webhooks](#testing-your-endpoint-before-you-trust-posteverywhere-webhooks) 8. [PostEverywhere webhook limits, and what they mean in practice](#posteverywhere-webhook-limits-and-what-they-mean-in-practice) 9. [FAQs](#faqs) ## What PostEverywhere webhooks replace Publishing to a social platform is not instant. A post is accepted, queued, handed to the platform's API, and then either succeeds or fails, sometimes seconds later and sometimes considerably longer for video. Anything you build on top of the [social media API](/social-media-api) has to learn the outcome somehow. Polling is the obvious answer and the wrong one. It costs requests against your [rate limit](/blog/social-media-api-rate-limits), it adds latency between the failure and your reaction to it, and it scales badly: ten scheduled posts is ten polling loops. A webhook inverts it. You register one HTTPS endpoint, tell PostEverywhere which events you care about, and receive a POST when they happen: ```bash curl -X POST https://app.posteverywhere.ai/api/v1/webhooks \ -H "Authorization: Bearer $POSTEVERYWHERE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/webhooks/posteverywhere", "events": ["post.published", "post.failed", "account.reconnect_needed"], "name": "Production" }' ``` The create response returns a `secret` beginning `whsec_`. **It is shown once and never again.** There is no endpoint to retrieve it later, so save it to your secret store before you close the terminal. If you lose it, the only remedy is deleting the webhook and creating a new one. > **Building on the API?** Webhooks, the REST API, the typed Node SDK and the [hosted MCP server](/mcp) all run on one key and are included on [every plan from $9/mo](/pricing). [Start a 7-day free trial](https://app.posteverywhere.ai/signup): card required, $0 charged today. ## Every PostEverywhere webhook event, and which ones actually fire PostEverywhere declares fifteen events. **Twelve of them are emitted today**, and three are reserved names that are not currently sent. That distinction matters more than it sounds, because building a handler around an event that never arrives is a silent failure you will not notice until production. | Event | Fires when | |---|---| | `post.scheduled` | A new post is created via `POST /v1/posts` | | `post.published` | A destination has successfully published | | `post.failed` | A destination has failed after all retries | | `post.partially_failed` | A post group ends with some destinations succeeded and some failed | | `post.deleted` | A post is deleted | | `account.disconnected` | A social account is disconnected by the user | | `account.reconnect_needed` | An account's token is detected dead and the user must reconnect | | `media.uploaded` | An **image** upload is finalised via `POST /v1/media/{id}/complete` | | `media.deleted` | A media item is deleted | | `post.approval_requested` | A draft is submitted for approval, carrying `requested_by` | | `post.approved` | An approver clears a pending post, carrying `reviewed_by` and any `note` | | `post.changes_requested` | A pending post is sent back to its author with the reviewer's `note` | The three declared but **not currently emitted**: `post.publishing`, `post.updated` and `account.connected`. Do not build on them yet. For those cases, poll `GET /v1/posts/:id` or `GET /v1/accounts` after the relevant action instead. Two further details worth knowing. `media.uploaded` fires for images only, not for video, documents or URL imports. And `post.partially_failed` exists because a single post can fan out to many destinations through [cross-posting](/cross-posting): four destinations can end as three published and one failed, which is neither a clean success nor a clean failure. New events are added over time without breaking existing subscriptions, so subscribe to what you need and ignore the rest. ## What a PostEverywhere webhook payload contains Every PostEverywhere delivery uses the same envelope: ```json { "event": "post.published", "event_id": "9c4d12e0-...", "created_at": "2026-06-11T05:42:18.234Z", "organization_id": "56cb4496-...", "data": { } } ``` The `data` object varies by event. For `post.published` it carries the identifiers you need to reconcile against your own records: ```json { "data": { "post_id": "a808f10f-...", "destination_id": "b9070ed5-...", "platform": "instagram", "account_id": 5356, "account_name": "yourbrand", "published_at": "2026-06-11T05:42:17.000Z", "platform_post_id": "17841451231344189_..." } } ``` `platform_post_id` is the identifier the network itself assigned, which is what you store if you later want to link to the live post or pull its [analytics](/social-media-analytics). **Treat the payload as forward-compatible.** Unknown fields may be added over time, so ignore anything you do not recognise rather than failing validation on it. A handler that rejects unexpected keys will break the first time the API gains a field. ## Headers PostEverywhere sends with every delivery | Header | Value | |---|---| | `Content-Type` | `application/json` | | `User-Agent` | `PostEverywhere-Webhook/1.0` | | `X-PostEverywhere-Event` | The event name, for example `post.published` | | `X-PostEverywhere-Event-Id` | Stable UUID. Use it as your dedup key | | `X-PostEverywhere-Delivery-Id` | Delivery UUID, constant across that delivery's retries | | `X-PostEverywhere-Timestamp` | Unix epoch seconds, for anti-replay | | `X-PostEverywhere-Signature` | `sha256=` | | `X-PostEverywhere-Attempt` | Attempt number, 1 to 6 | The pair worth internalising is `Event-Id` and `Delivery-Id`. **Delivery is at-least-once**, so the same event can legitimately arrive twice. Dedupe on `X-PostEverywhere-Event-Id` and your handler becomes safe to retry. `Delivery-Id` stays constant across the retries of one delivery, which is what you want in your logs when you are working out why attempt 4 arrived six hours after attempt 1. ## How to verify a PostEverywhere webhook signature **Verify every incoming webhook.** Your endpoint is a public URL. Without verification, anyone who discovers it can POST a fake `post.published` and your system will believe it. ```ts import crypto from "crypto"; function verifyWebhook(rawBody: string, signatureHeader: string, secret: string): boolean { if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false; const provided = signatureHeader.slice(7); const expected = crypto.createHmac("sha256", secret) .update(rawBody) .digest("hex"); try { return crypto.timingSafeEqual( Buffer.from(provided, "hex"), Buffer.from(expected, "hex"), ); } catch { return false; } } ``` The signature is HMAC-SHA256, specified in RFC 6234, computed with Node's crypto module or Python's hmac equivalent. Three mistakes account for nearly every "the signature never matches" report: **Use the raw request body.** Not a re-serialised object. Parsing JSON and stringifying it again can reorder keys or change whitespace, and the HMAC is computed over bytes. In Express that means `express.raw()` before any JSON middleware touches it. **Use a constant-time compare.** `crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python. A naive `===` leaks timing information that can be used to forge a signature byte by byte. **Check the timestamp.** Reject deliveries where `X-PostEverywhere-Timestamp` is older than about five minutes. HMAC alone proves the payload came from us; the timestamp stops someone replaying a captured payload later. The full walkthrough with Python and Ruby versions is in [verifying PostEverywhere webhook signatures](/blog/verify-posteverywhere-webhook-signatures). ## How PostEverywhere retries a failed delivery If your endpoint returns anything other than a `2xx` within **10 seconds**, PostEverywhere retries with exponential backoff: | Attempt | Delay before this attempt | |---|---| | 1 | immediate | | 2 | 30 seconds | | 3 | 2 minutes | | 4 | 10 minutes | | 5 | 1 hour | | 6 | 6 hours | After **6 failed attempts** the delivery is marked `dead` and never retried. After **20 consecutive failures on the same webhook**, the webhook is **auto-disabled** to stop us hammering a dead endpoint. Re-enable it with `PATCH /v1/webhooks/:id` and `{"is_active": true}`, which also resets the failure counter. That ladder spans just under eight hours, which is deliberate: it is long enough to survive a deploy or a short outage, and short enough that a genuinely dead endpoint stops consuming attempts. The practical consequence is that **a webhook you never fixed will silently switch itself off**, so alert on your own 5xx rate rather than assuming silence means nothing happened. > **Want to see the whole tool surface first?** The [MCP server page](/mcp) lists all 38 tools by group, and the [developer docs](/developers) cover every endpoint including the full [webhooks reference](/docs/webhooks). ## Testing your endpoint before you trust PostEverywhere webhooks Send a synthetic delivery before you depend on it: ```bash curl -X POST https://app.posteverywhere.ai/api/v1/webhooks/$WEBHOOK_ID/test \ -H "Authorization: Bearer $POSTEVERYWHERE_API_KEY" ``` The response reports what your receiver actually did, including its HTTP status and how long it took: ```json { "data": { "ok": true, "status": 200, "duration_ms": 87 } } ``` The test payload carries `data.test: true`, so you can detect and ignore it in your handler rather than creating a phantom record. If `duration_ms` is creeping toward 10,000 you have already found your next problem. ## PostEverywhere webhook limits, and what they mean in practice - **25 webhooks per organisation.** Contact support to raise it. In practice one endpoint per environment is the sane pattern, not one per event. - **HTTPS only in production.** Plain HTTP is refused, as are localhost and private IPs, which is SSRF protection. Use a tunnel with a public HTTPS hostname for local development. - **10-second timeout per delivery.** Return `200` immediately and process asynchronously. Verify, dedupe, enqueue, respond. - **2 KB of your response body is captured** for debugging. Put something useful in it. The recommended handler shape follows directly from those limits: ```ts const rawBody = await req.text(); if (!verifyWebhook(rawBody, req.headers.get('x-posteverywhere-signature') || '', secret)) { return new Response('Invalid signature', { status: 401 }); } const eventId = req.headers.get('x-posteverywhere-event-id'); if (await alreadyProcessed(eventId)) return new Response('OK', { status: 200 }); await queue.add('posteverywhere-event', JSON.parse(rawBody)); return new Response('OK', { status: 200 }); ``` Read raw, verify, dedupe, enqueue, return fast. Everything slow happens in the worker, where a failure is yours to retry rather than ours. If you are building an agent rather than a webhook receiver, the same events are what stop it polling. The [agents page](/agents) covers the patterns people build on this surface: our guide to [building an AI social media agent](/blog/build-an-ai-social-media-agent-with-posteverywhere) uses them, and [why your AI agent should not poll PostEverywhere](/blog/ai-agents-webhooks-not-polling) covers the pattern directly. ## FAQs ### What are PostEverywhere webhooks? PostEverywhere webhooks are real-time HTTPS notifications. Instead of polling the API to ask whether a post published, you register an endpoint and PostEverywhere POSTs a signed JSON payload the moment the event happens: a destination publishes, a post fails, or a social account needs reconnecting. ### How many webhook events does PostEverywhere support? Fifteen events are declared and twelve are emitted today. The three currently reserved are `post.publishing`, `post.updated` and `account.connected`. For those, poll `GET /v1/posts/:id` or `GET /v1/accounts` instead of waiting for a delivery that will not arrive. ### Where do I find my PostEverywhere webhook signing secret? It is returned once, in the `POST /v1/webhooks` create response, as a value beginning `whsec_`. There is no API to retrieve it afterwards. If you lose it, delete the webhook and create a new one. ### How do I verify a PostEverywhere webhook signature? Compute HMAC-SHA256 over the **raw** request body using your signing secret, and compare it to the `X-PostEverywhere-Signature` header using a constant-time compare. Do not re-serialise the JSON before hashing, and reject deliveries whose `X-PostEverywhere-Timestamp` is more than about five minutes old. ### What happens if my endpoint is down? PostEverywhere retries six times with exponential backoff: immediate, 30 seconds, 2 minutes, 10 minutes, 1 hour, then 6 hours. After six failures the delivery is dead. After 20 consecutive failures the webhook is auto-disabled and must be re-enabled with `PATCH /v1/webhooks/:id`. ### Can PostEverywhere deliver the same event twice? Yes. Delivery is at-least-once, so dedupe on the `X-PostEverywhere-Event-Id` header, which is stable for a given event. `X-PostEverywhere-Delivery-Id` stays constant across the retries of one delivery, which is the one you want in logs. ### Do webhooks cost extra on PostEverywhere? No. Webhooks, the REST API, the Node SDK, the CLI and the hosted MCP server are included on every plan from $9/mo. The 7-day free trial covers all of it, though a card is required to start and there is no free plan. ### Why is my webhook receiving nothing at all? Check three things in order: the webhook is `is_active: true` and has not been auto-disabled by 20 consecutive failures, your URL is public HTTPS rather than localhost or a private IP, and you subscribed to an event that is actually emitted rather than one of the three reserved names. --- Webhooks are the difference between an integration that tells you something broke and one you have to interrogate. Register one endpoint, verify the signature, dedupe on the event id, and the publish pipeline stops being a thing you poll. [Start your 7-day free trial](https://app.posteverywhere.ai/signup) and point your first webhook at a test endpoint in the next ten minutes. For the surrounding surface, the [social media scheduling API guide](/blog/social-media-scheduling-api-guide) walks the REST side end to end, and [API vs MCP](/blog/social-media-api-vs-mcp) covers when to drive PostEverywhere with code rather than an assistant.