Browse docs
/postsList posts
- operationId
- listPosts
- Required scope
- posts.read
- Tag
- Posts
The social media posting API, endpoint by endpoint#
Every table below lists the real /v1 paths, the scope each one requires, and what it does — the same registry the API validates requests against.
Posts#
{
"clientId": "6d58a0f2-1c3e-4b7a-9f21-0a1b2c3d4e5f",
"content": "Launch day is here 🚀",
"mediaIds": [],
"targets": { "mode": "selection", "accountIds": ["a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"] },
"schedule": { "type": "at", "scheduledAt": "2026-07-01T15:00:00Z" }
}The full compose body#
The minimal body above is enough to publish, but POST /posts accepts a lot more: per-platform overrides, a video cover, thread mode, AI-content disclosure, draft/approval routing, and campaign grouping. Every field below is part of the real contract — send only what you need, everything else defaults.
Post body — every top-level field#
Per-platform variant overrides (`variants[].overrides`)#
overrides is a free-form object — Record<string, unknown> on the wire. Each platform adapter reads only the keys it declares and ignores everything else, so overrides is safe to keep flat: send youtube keys on a youtube variant, tiktok keys on a tiktok variant, and so on.
The tables below list, per platform, the override keys SkedCast's adapters actually read today — verified against the adapter code, not just what a UI happens to render. A field marked Required means the platform rejects a publish without it (the target fails rather than silently degrading); everything else is optional with the stated default.
X (Twitter) — variant overrides#
LinkedIn — variant overrides#
Instagram — variant overrides#
Facebook — variant overrides#
TikTok — variant overrides#
YouTube — variant overrides#
Threads — variant overrides#
Pinterest — variant overrides#
Bluesky — variant overrides#
Telegram — variant overrides#
{
"clientId": "6d58a0f2-1c3e-4b7a-9f21-0a1b2c3d4e5f",
"content": "Behind the scenes of launch day 🎬",
"mediaIds": ["9b1e2c3d-4f5a-4b6c-8d9e-0f1a2b3c4d5e"],
"targets": { "mode": "selection", "accountIds": ["a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"] },
"variants": [
{
"platform": "tiktok",
"overrides": {
"privacyLevel": "PUBLIC_TO_EVERYONE",
"disableComment": false,
"disableDuet": true
}
}
],
"schedule": { "type": "at", "scheduledAt": "2026-07-01T15:00:00Z" }
}Post targets (per-destination lifecycle)#
Clients & accounts#
Managing clients#
The two write scopes on this surface are clients.manage (create / update / archive a client) and accounts.connect (mint connect links). Both are Owner/Manager actions — a token's effective power is still scopes ∩ role, so a Manager token can create and update clients but the archive (DELETE) is Owner-only.
POST /clients is idempotent on (agency, name); replaying the same Idempotency-Key returns the original client instead of a duplicate. PATCH /clients/:id is a partial update — only the fields you send change.
{
"name": "Acme Studios",
"type": "brand",
"defaultPostMode": "now"
}Connect links#
A connect link lets a client connect their OWN social accounts without a SkedCast login. You mint one on demand with POST /clients/:id/connect-invites, share the returned url, and the recipient authorizes each requested platform straight from that page.
Each link is bound to a SINGLE client and carries the exact platforms you requested (platforms, ≥1). It is valid until expiresAt (expiresInHours, 1–168, default 48) and is multi-use within that window — one link lets the recipient connect every requested platform, returning as many times as they need. Optionally record a recipient email for your own tracking.
The url has the shape ${CONSOLE_URL}/invite/<token> (the token is also returned raw). Send the whole url; the token IS the authorization for that page — treat it like a secret.
curl -X POST https://api.skedcast.com/v1/clients/<clientId>/connect-invites \
-H "Authorization: Bearer sked_live_YOUR_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "platforms": ["instagram", "tiktok"], "expiresInHours": 72 }'{
"data": {
"inviteId": "3f9a…",
"url": "https://app.skedcast.com/invite/inv_9f3a2b…",
"token": "inv_9f3a2b…",
"platformsRequested": ["instagram", "tiktok"],
"expiresAt": "2026-07-06T15:00:00Z"
},
"meta": { "correlationId": "c-…" }
}Analytics#
Analytics endpoints accept an optional date window (from/to as YYYY-MM-DD, default last 30 days) plus optional platform, clientId, and socialAccountId filters. Metrics a platform doesn't expose come back as null, never a fabricated 0.
That distinction is load-bearing and worth reading twice. null means *this platform does not report this number*, 0 means *the platform reported zero*, and an absent key means *we have not fetched it yet*. Charting null as 0 is the single most common way to draw a graph that says something untrue about a customer's account.
Three grains, and which one to ask for#
Analytics exists at three grains, and picking the wrong one is the usual reason a number looks wrong. They do not roll up into one another, because the platforms do not report them that way.
- **Workspace** —
GET /v1/analytics/overview. Everything you published, summed, with a per-platform and per-account breakdown plus a daily series. - **Account** —
GET /v1/analytics/accounts/{id}. One connected channel. The only place profile-level activity and follower demographics appear. - **Post** —
GET /v1/analytics/postsand/v1/analytics/posts/{id}. One published post's lifetime counters, broken down per platform target.
Post metrics — every field, and what it counts#
Account metrics — `GET /v1/analytics/accounts/{id}`#
Follower demographics#
{
"object": "account_analytics",
"socialAccountId": "acc_01HZ...",
"platform": "instagram",
"audience": { "profileViews": 412, "profileLinkTaps": 37 },
"demographics": {
"country": { "US": 1204, "GB": 388, "PK": 91 },
"city": { "London, England": 210 },
"age": { "18-24": 402, "25-34": 899 },
"gender": { "F": 720, "M": 640, "U": 41 }
}
}Freshness, and why a number can be older than you expect#
Every analytics payload carries asOf — the freshest underlying snapshot in the window, not the time you made the request. Post rows carry their own fetchedAt. A nightly worker ingests metrics; POST /analytics/refresh (first-party only) triggers an on-demand pull.
Some lag is the platform's, not ours. YouTube Analytics settles roughly 48–72 hours behind, so a video published yesterday legitimately reports nulls on the rich fields. Instagram Stories expire after 24 hours and their insights stop updating at expiry — highlighting a story does not extend that window.
Reports & best times#
Media (content library)#
Uploading is a presign → PUT → register sequence, and registering (or importing) is what starts transcoding — the asset is pending until then. All three POSTs require an Idempotency-Key header, and clientId is optional on all three (an asset can be agency-wide).
The optional platforms array on POST /media and POST /media/import-url narrows which platform-specific renditions get produced. Omit it and SkedCast prepares a rendition for every platform you have a live connected account on (scoped to clientId when you send one); pass e.g. platforms: ["youtube"] to narrow further. If a post later targets a platform with no rendition yet, the publish pipeline fills the gap on demand rather than failing. See Uploading media & video covers for the full worked flow, including how to attach a custom video cover.
Webhooks (event-driven)#
FAQ
- Is there an OpenAPI spec?
- Yes — https://skedcast.com/openapi.json is a full OpenAPI 3.1 document covering every /v1 operation, the security schemes, and the error model. Use it for Postman, code generation, or AI tooling.
- What happens if I send an unknown key inside variants[].overrides?
- It's silently ignored — overrides has no per-platform schema validation, so a typo or a key from the wrong platform never errors, it just does nothing. Check the field name against the per-platform tables above.
- How do I disclose AI-generated content?
- Set the top-level aiDisclosure boolean on POST /posts. It's copied onto every variant's aiGenerated override automatically at publish time — YouTube sets containsSyntheticMedia and TikTok sets its AIGC flag from it. You don't need to set aiGenerated yourself in overrides.