Works with all AI agents

Connect your AI agent

Choose your AI agent and connect in seconds.

Add to Claude

Works on every Claude plan, including free. Full Claude guide

  1. 1

    Add to Claude

    One click from Claude’s connector directory.

  2. 2

    Sign up or sign in

    Takes seconds, then you’re sent straight back.

  3. 3

    Ask Claude

    “Plan and schedule my posts for next week.”

PostEverywhere Webhooks Guide

Receive real-time HTTPS notifications when posts publish or fail, accounts disconnect, or media uploads complete. Includes HMAC-SHA256 signing, retry policy, and verification examples.

Webhooks let you receive real-time HTTPS notifications the moment something interesting happens in PostEverywhere, instead of polling /v1/posts/{id} every few seconds waiting for a publish to finish.

Subscribe once, and PostEverywhere will POST a signed JSON payload to your endpoint every time a matching event occurs.

Set up your first webhook

# 1. Create a webhook subscription
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"
  }'

Response (the secret is shown once only, save it now):

{
  "data": {
    "id": "9f3c4d12-...",
    "url": "https://your-app.example.com/webhooks/posteverywhere",
    "events": ["post.published", "post.failed", "account.reconnect_needed"],
    "secret": "whsec_abc123...64chars",
    "secret_warning": "Save this secret now — it is shown only once.",
    "is_active": true,
    "created_at": "2026-06-11T...",
    ...
  },
  "error": null,
  "meta": { "request_id": "a1b2c3d4", "timestamp": "2026-06-11T09:55:12.341Z" }
}

Every webhook event and when it fires

Event Fires when
post.scheduled A post is created to publish now or later (in the app, with POST /v1/posts or the MCP create_post), or a draft is scheduled
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 + some failed
post.warning A destination published but carries a warning that needs attention, for example text was cut, a setting was not applied or TikTok left a public post private (data carries warnings[] with code and message)
post.deleted A post is deleted
account.disconnected A social account is disconnected by the user
account.reconnect_needed A social account's token is detected dead (user must reconnect)
media.uploaded An image upload is finalised via POST /v1/media/{id}/complete (not emitted for video, documents or URL imports)
media.deleted A media item is deleted
post.approval_requested A draft is submitted for approval (data carries requested_by)
post.approved An approver clears a pending post; it can now be scheduled (data carries reviewed_by and any note)
post.changes_requested A pending post is sent back to its author (data carries the reviewer's note)

Subscribe to one or many. New events are added over time without breaking existing subscriptions. There is no event for an edited post or a newly connected account: after a connect link, poll GET /v1/accounts.

Every publish-time warning (text cut to fit, a setting the platform did not apply, a privacy change) sends post.warning. The same warnings are on the post: read warnings for each platform in GET /v1/posts/:id/results. See Published, but with a warning.

What a webhook payload contains

Every delivery has this envelope:

{
  "event":            "post.published",
  "event_id":         "9c4d12e0-...",
  "created_at":       "2026-06-11T05:42:18.234Z",
  "organization_id":  "56cb4496-...",
  "data":             { ... event-specific fields ... }
}

The data shape depends on the event. Example for post.published:

{
  "event": "post.published",
  "event_id": "9c4d12e0-...",
  "created_at": "2026-06-11T05:42:18.234Z",
  "organization_id": "56cb4496-...",
  "data": {
    "post_id":           "a808f10f-...",
    "destination_id":    "b9070ed5-...",
    "platform":          "instagram",
    "account_id":        5356,
    "account_name":      "taxrefundonspot.com.au",
    "published_at":      "2026-06-11T05:42:17.000Z",
    "platform_post_id":  "17841451231344189_..."
  }
}

Unknown fields may be added over time. Receivers should treat the payload as forward-compatible: ignore fields you don't recognise.

Headers sent with every webhook

Every delivery includes these headers:

Header Value
Content-Type application/json
User-Agent PostEverywhere-Webhook/1.0
X-PostEverywhere-Event The event name (e.g. post.published)
X-PostEverywhere-Event-Id Stable UUID: use as a dedup key (we may deliver at-least-once)
X-PostEverywhere-Delivery-Id Delivery UUID, constant across that delivery's retries (X-PostEverywhere-Attempt identifies the attempt)
X-PostEverywhere-Timestamp Unix epoch seconds (anti-replay)
X-PostEverywhere-Signature sha256=<HMAC-SHA256(body, secret)>
X-PostEverywhere-Attempt Attempt number (1-6)

Verifying the PostEverywhere webhook signature

You MUST verify the signature on every incoming webhook. Without verification, anyone could POST to your URL and pretend to be PostEverywhere.

Node.js / TypeScript

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); // strip "sha256="
  const expected = crypto.createHmac("sha256", secret)
    .update(rawBody) // ⚠️ MUST be the raw body before any parsing
    .digest("hex");

  // Constant-time compare to prevent timing attacks.
  try {
    return crypto.timingSafeEqual(
      Buffer.from(provided, "hex"),
      Buffer.from(expected, "hex"),
    );
  } catch {
    return false;
  }
}

Python

import hmac
import hashlib

def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not signature_header.startswith("sha256="):
        return False
    provided = signature_header[7:]
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(provided, expected)

Ruby

require "openssl"

def verify_webhook(raw_body, signature_header, secret)
  return false unless signature_header&.start_with?("sha256=")
  provided = signature_header.sub("sha256=", "")
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
  Rack::Utils.secure_compare(provided, expected)
end

Important verification gotchas

  • Use the RAW request body, not a re-serialised JSON object. JSON re-encoding may reorder keys or change whitespace and the HMAC will not match.
  • Use a constant-time compare (crypto.timingSafeEqual / hmac.compare_digest). Naive == leaks timing information.
  • Optionally check X-PostEverywhere-Timestamp: reject deliveries older than ~5 minutes to prevent replay of a captured payload.
  • Dedupe by X-PostEverywhere-Event-Id: delivery is at-least-once. Idempotent processing prevents double-acting on rare retries.

How PostEverywhere retries failed deliveries

If your endpoint returns anything other than 2xx within 10 seconds, the delivery is retried 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 not retried again.

After 20 consecutive failed deliveries on the same webhook, the webhook is auto-disabled. You can re-enable it via PATCH /v1/webhooks/:id with {"is_active": true}, which also resets the failure counter.

Testing your webhook endpoint

Before flipping a webhook to production, send a synthetic ping:

curl -X POST https://app.posteverywhere.ai/api/v1/webhooks/$WEBHOOK_ID/test \
  -H "Authorization: Bearer $POSTEVERYWHERE_API_KEY"

Response shows the receiver's HTTP status + duration:

{
  "data": {
    "ok": true,
    "status": 200,
    "duration_ms": 87,
    "message": "Test webhook delivered successfully. Verify your endpoint received the X-PostEverywhere-Signature header and validated it against your saved secret."
  },
  "error": null,
  "meta": { "request_id": "a1b2c3d4", "timestamp": "2026-06-11T09:55:12.341Z" }
}

The test payload includes data.test: true so you can detect and ignore it in your handler if you want.

Creating, listing and deleting subscriptions

Related: testing webhook delivery · webhook validation errors · reconnect_needed and account health · webhook event changes

Full reference: List · Create · Get · Update · Delete · Test.

# List your webhooks
curl https://app.posteverywhere.ai/api/v1/webhooks \
  -H "Authorization: Bearer $POSTEVERYWHERE_API_KEY"

# Update events / URL / active state
curl -X PATCH https://app.posteverywhere.ai/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $POSTEVERYWHERE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events": ["post.published","post.failed","post.partially_failed"]}'

# Disable temporarily
curl -X PATCH https://app.posteverywhere.ai/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $POSTEVERYWHERE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'

# Delete (also cascades delivery history)
curl -X DELETE https://app.posteverywhere.ai/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $POSTEVERYWHERE_API_KEY"

PostEverywhere webhook limits per organisation

  • 25 webhooks per organization (contact support to raise)
  • HTTPS only in production (HTTP refused). Localhost / private IPs refused (SSRF protection).
  • 10-second timeout per delivery
  • 2-KB response body capture for debugging (longer responses are truncated)

Common questions about webhooks

Where is the signing secret? It's not in the GET response.

The signing secret is returned once only in the POST /v1/webhooks create response. There is no API to retrieve it later. If you lost it, delete the webhook and create a new one.

What if my endpoint is slow?

You have 10 seconds to respond with a 2xx status. If your processing is slow, return 200 immediately and process the event asynchronously in your worker / queue. Idempotent processing keyed on event_id makes retries safe.

How do I prevent replay attacks?

Verify the X-PostEverywhere-Timestamp header is within the last 5 minutes. Combined with HMAC verification, this prevents an attacker who captures a payload from re-submitting it later.

What's a sensible processing pattern?

// 1. Read raw body BEFORE parsing.
const rawBody = await req.text();
// 2. Verify signature.
if (!verifyWebhook(rawBody, req.headers.get('x-posteverywhere-signature') || '', secret)) {
  return new Response('Invalid signature', { status: 401 });
}
// 3. Dedupe.
const eventId = req.headers.get('x-posteverywhere-event-id');
if (await alreadyProcessed(eventId)) return new Response('OK', { status: 200 });
// 4. Parse.
const event = JSON.parse(rawBody);
// 5. Enqueue + return fast.
await queue.add('posteverywhere-event', event);
return new Response('OK', { status: 200 });

How do I subscribe to all events?

Pass the full event list explicitly. There's no * wildcard: this is intentional, so adding a new event type in the future doesn't silently start firing to your endpoint without you opting in.