Browse docs
Why subscribe instead of poll#
Every state change fires an event at the instant it commits — the same code path that writes your Activity Log. A poll sees the result later, costs a request per tick, and has to know which field to wait on; a subscription is told. The register response for an upload even names the event to wait for (readyEvent: "media.ready").
Polling still works everywhere and is the right choice when your integration cannot accept inbound HTTP (a laptop script, a locked-down CI runner). If you poll media, wait on publishable: true, not on a platform appearing in variants[].
curl -X POST https://api.skedcast.com/v1/webhooks \
-H "Authorization: Bearer sked_live_YOUR_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/skedcast/events",
"events": ["media.ready", "media.failed", "target.published", "target.failed", "account.reauth_required"]
}'What a delivery looks like#
Every delivery is a POST with a JSON envelope { id, type, created, data } and two independent signature schemes: the Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature — verify with any off-the-shelf library) and SkedCast's own X-SkedCast-Signature: t=<unix-seconds>,v1=<hex> (HMAC-SHA256 over "<t>.<rawBody>"). Verify one; both are always present.
Delivery is at-least-once with exponential-backoff retries; X-SkedCast-Attempt counts them. Dedupe on the payload id — it is stable across retries. Events that can be observed twice on our side (a replayed job, two workers seeing one transition) are minted at-most-once for the fact itself: post.partially_published, post.canceled, media.failed, media.quarantined, media.variant.*, account.reauth_required.
{
"id": "5f0c2d4e-3b1a-4c8e-9d7f-0a1b2c3d4e5f",
"type": "media.ready",
"created": "2026-09-02T19:39:37.129Z",
"data": { "assetId": "eb1882be-1686-4199-a169-1d6cece2eaf2", "status": "transcoded" }
}Posts#
Destinations (one per connected account)#
Media#
Connected accounts#
Approvals#
Analytics#
Reports#
Clients#
Campaigns#
Team#
Secret rotation without a gap#
POST /webhooks/:id/rotate-secret returns the new secret in full (once). The previous secret keeps being signed with for 24 hours, and during that window both signature headers carry one entry per valid secret — a receiver is correct if ANY entry matches. Rotate, deploy the new secret at your leisure, and no event is dropped in between.
FAQ
- Do I need a public endpoint?
- For webhooks, yes — an HTTPS URL we can POST to. If you cannot expose one, poll instead: GET /media/:id (wait on publishable), GET /post-targets (per-destination status), GET /accounts (health). Everything a webhook says is also readable.
- Why did I get the same event twice?
- Delivery is at-least-once: if your endpoint processed an event but we never saw the 2xx (a timeout, a dropped connection), we retry. The payload id is stable across retries — dedupe on it. Facts that can be observed twice on our side are already minted at-most-once, so a duplicate is always a retry, never a second occurrence.
- Which event tells me a video is ready to publish?
- media.ready. The register response names it as readyEvent so you never have to look it up. If a per-platform rendition is produced later (at ingest or during a publish-time fill) you also get media.variant.ready or media.variant.failed for that platform; the asset-level media.ready is the one that means "you can compose against this now".