postmatic

Webhooks

Receive a signed POST when a post is scheduled or finishes, verify the signature, handle retries and check the delivery history.

Instead of polling GET /v1/posts/:id in a loop, register a URL and Postmatic tells you.

Events

EventWhen
post.scheduledA schedule was accepted (POST /v1/posts with scheduledFor, or a scheduled retry).
post.publishedEvery target published.
post.partialAt least one target published and at least one failed.
post.failedEvery target failed (including a missed schedule, MISSED).

A scheduled post emits post.scheduled and then one final event. An immediate post emits only the final one. Cancelling through the API emits nothing. A POST /v1/posts/:id/retry produces a new final event once it finishes.

Subscribing

POST /v1/notifications
curl -X POST https://api.uat.postmatic.dev/v1/notifications \
  -H "x-access-key: pm_live_…" \
  -H "content-type: application/json" \
  -d '{ "eventTypes": ["post.published", "post.partial", "post.failed"], "webhook": { "url": "https://your-app.com/hooks/postmatic" } }'

The response carries webhook.secret (whsec_…) once. Save it. profileId restricts to a profile's posts; onlyScheduledPosts: true sends final events only for posts that were scheduled.

Test the destination before or after saving it with POST /v1/notifications/test ({ "notificationId": "ntf_…" } or { "webhook": { "url": "…" } }).

Verifying the signature

Every delivery is a POST with Content-Type: application/json and these headers:

HeaderValue
X-Postmatic-Event-IdThe event's id (evt_…). Use it as the deduplication key.
X-Postmatic-Event-TypeE.g. post.published.
X-Postmatic-TimestampUnix timestamp in seconds.
X-Postmatic-Signaturesha256= + HMAC-SHA256 hex of <timestamp>.<raw body>.
Node
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret: string, headers: Record<string, string>, rawBody: string): boolean {
  const ts = headers['x-postmatic-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 min tolerance
  const expected = `sha256=${createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')}`;
  const given = headers['x-postmatic-signature'] ?? '';
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Sign over the raw body (do not re-serialize the JSON). Answer 2xx in under 10 seconds and process afterward.

Payload

json
{
  "version": "2026-09-01",
  "id": "evt_…",
  "type": "post.published",
  "createdAt": "2026-09-09T12:00:00.000Z",
  "projectId": "proj_…",
  "profileId": null,
  "data": {
    "post": {
      "id": "post_…", "status": "published", "content": "Now live.",
      "scheduledFor": null, "publishedAt": "2026-09-09T12:00:00.000Z",
      "mediaItems": [{ "type": "image", "url": "https://…/photo.jpg" }],
      "links": { "api": "https://api.uat.postmatic.dev/v1/posts/post_…" }
    },
    "platforms": [{
      "platform": "instagram", "integrationId": "int_…", "profileId": null,
      "status": "published", "platformPostId": "1790…", "platformPostUrl": "https://www.instagram.com/p/…/",
      "publishedAt": "2026-09-09T12:00:00.000Z", "errorCode": null, "errorMessage": null, "warningMessage": null
    }]
  }
}

Retries

Without a 2xx in 10 seconds, Postmatic retries after 30 s and after 2 min (3 attempts total), with the same body and the same id. After that the delivery stays failed in the history. Duplicates are possible: deduplicate by id.

History

GET /v1/notifications/:id/deliveries lists every event delivered to the subscription, with status (pending, delivered, failed), attempt, responseStatus and error.

Webhooks — Postmatic Documentation