PostZen

Webhooks

Receive post outcomes and account health events from PostZen in real time.

Webhooks let your application react when a post finishes publishing or a connected account needs attention. PostZen sends an HTTPS POST request to every active endpoint subscribed to the event.

Webhook delivery is at least once. Store and deduplicate on the stable X-PostZen-Event-Id value before applying side effects.

Set up an endpoint

  1. Open Webhooks in the dashboard and select Create webhook.
  2. Enter a public HTTPS URL, choose at least one event, and select the profiles the endpoint can receive.
  3. Add a signing secret. PostZen permits unsigned webhooks, but signing is strongly recommended for every production endpoint.
  4. Add any custom authentication headers your receiver needs, save the endpoint, then send a test event.

You can also manage endpoints with the Webhooks API reference. Signing secrets and custom header values are encrypted at rest and are never returned after saving.

Events

EventSent when
post.publishedEvery target for a post has published successfully.
post.partially_failedAt least one target published and at least one target failed.
post.failedA post reaches a terminal failure without a successful target.
account.needs_reauthA connected account first changes to needs_reauth.
account.disconnectedA provider first reports that an account is disconnected.
webhook.testYou send a test from the dashboard or API.

Repeated writes of the same status do not create another event. If an account reconnects and later needs reauthorization again, that later transition creates a new event.

Payload

Every request has the same top-level envelope. The data object varies by event type.

{
  "id": "evt_01j4z6vy7k8x9m2q3r4s5t6uvw",
  "type": "post.published",
  "apiVersion": "2026-08-06",
  "createdAt": "2026-08-06T18:42:10.123Z",
  "data": {
    "post": {
      "id": "post_01j4z6r8p4fmw7k3n2q1t9abcd",
      "profileId": "profile_01j4z5zj8ab2q6t7m3n4p5cdef",
      "status": "published",
      "scheduledFor": "2026-08-06T18:40:00.000Z",
      "targets": [
        {
          "accountId": "account_01j4z5x1m8n7q3p2r6t9uvwxyz",
          "platform": "linkedin",
          "status": "published",
          "platformPostUrl": "https://www.linkedin.com/feed/update/urn:li:share:123"
        }
      ]
    }
  }
}

Account events include the account ID, profile ID, platform, username, status, and a machine-readable reason when one is available. Payloads never include access tokens, API keys, signing secrets, custom header values, billing data, or provider credentials.

Request headers

HeaderDescription
Content-TypeAlways application/json.
X-PostZen-EventEvent type, such as post.published.
X-PostZen-Event-IdStable event ID shared by retries and manual redeliveries.
X-PostZen-Delivery-IdDelivery log record ID.
X-PostZen-TimestampUnix timestamp in seconds for this delivery attempt.
X-PostZen-Signaturev1=<hex> HMAC signature. Present only when the endpoint has a secret.

Configured custom headers are added after PostZen's reserved headers. Retries, tests, and manual redeliveries use the endpoint's current URL, secret, and custom header values.

Retry schedule

PostZen treats every 2xx response as success. Network errors, timeouts, 429 responses, and other non-2xx responses are retried. Each request has a 10-second timeout.

AttemptTiming
1Immediately
21 minute after the previous attempt
35 minutes after the previous attempt
430 minutes after the previous attempt
52 hours after the previous attempt
68 hours after the previous attempt

When a response includes Retry-After, PostZen waits for the longer of that value and the normal retry delay, capped at 24 hours. An endpoint is disabled after 10 consecutive events exhaust all six attempts. A successful delivery resets its consecutive failure count.

Verify signatures

When an endpoint has a secret, PostZen signs the exact raw request body. Build the signed value as:

<timestamp>.<raw request body>

Then calculate HMAC-SHA256 with the endpoint secret and compare v1=<hex digest> with X-PostZen-Signature using a constant-time comparison. Do not parse and reserialize the JSON before verification because that changes the signed bytes.

Reject requests whose X-PostZen-Timestamp is more than five minutes old. You should also reject timestamps too far in the future and deduplicate the event ID to prevent replayed side effects.

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyPostZenWebhook({ rawBody, secret, signature, timestamp }) {
	const timestampSeconds = Number(timestamp)

	if (!Number.isFinite(timestampSeconds)) return false
	if (Math.abs(Date.now() / 1000 - timestampSeconds) > 5 * 60) return false

	const digest = createHmac('sha256', secret)
		.update(`${timestamp}.${rawBody}`)
		.digest('hex')
	const expected = Buffer.from(`v1=${digest}`, 'utf8')
	const received = Buffer.from(signature, 'utf8')

	return expected.length === received.length && timingSafeEqual(expected, received)
}

Pass the body to this function exactly as your HTTP framework received it, before JSON parsing.

Python

import hashlib
import hmac
import time


def verify_postzen_webhook(raw_body: bytes, secret: str, signature: str, timestamp: str) -> bool:
    try:
        timestamp_seconds = int(timestamp)
    except ValueError:
        return False

    if abs(time.time() - timestamp_seconds) > 5 * 60:
        return False

    signed_payload = timestamp.encode("utf-8") + b"." + raw_body
    digest = hmac.new(
        secret.encode("utf-8"),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(f"v1={digest}", signature)

Use the original request bytes as raw_body, not the output of json.dumps().

Operational guidance

  • Return a 2xx response quickly, then process the event asynchronously.
  • Deduplicate by event ID before applying side effects.
  • Keep signing enabled for production endpoints, even though unsigned endpoints are permitted.
  • Use the delivery logs to inspect response status, timing, excerpts, and the next retry.
  • Rotate an expired custom authentication value on the endpoint; the next retry reads the current value.

On this page