logo
Schedulin Developers

Webhooks

Instead of polling GET /v0/posts/{id} or GET /v0/studio/getGeneration, register a webhook endpoint and Schedulin will POST to your URL when things happen.

Events

EventFires when
post.publishedA post finished publishing to its platform
post.failedA publish attempt terminally failed
generation.completedAn AI image, video, or portrait generation finished rendering
generation.failedAn AI generation failed

Register an endpoint

curl -X POST https://api.schedulin.app/v0/webhooks \
  -H "x-api-key: $SCHEDULIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/schedulin",
    "events": ["post.published", "post.failed"],
    "description": "Production notifications"
  }'

The response includes the signing secret (whsec_...) once — store it. Subsequent reads return a masked value. Rotate it any time with POST /v0/webhooks/{id}/rotate-secret.

URLs must be https. Each organization can register up to 10 endpoints. OAuth apps need the webhooks:read / webhooks:write scopes.

Delivery format

{
  "id": "d2f1…",
  "event": "post.published",
  "createdAt": "2026-07-28T15:04:05.000Z",
  "data": {
    "postId": "…",
    "socialAccountId": "…",
    "platform": "TIKTOK",
    "status": "COMPLETED"
  }
}

Headers on every delivery:

HeaderMeaning
x-schedulin-eventThe event name
x-schedulin-delivery-idUnique delivery id (use for idempotency)
x-schedulin-signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

Verifying signatures

The signature is an HMAC-SHA256 of `${t}.${rawBody}` using your endpoint secret:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=") as [string, string]),
  );
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return (
    fresh &&
    timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"))
  );
}

Reject deliveries older than 5 minutes to prevent replay.

Retries and auto-disable

Respond with any 2xx within 10 seconds. Anything else (including timeouts) is retried with exponential backoff for up to 5 attempts. After 15 consecutive terminally-failed deliveries the endpoint is disabled (enabled: false); fix your receiver and re-enable it with PATCH /v0/webhooks/{id}.

Testing

POST /v0/webhooks/{id}/test sends a signed ping event to your URL. Delivery history — status, attempts, last response code — is at GET /v0/webhooks/{id}/deliveries.