How to Schedule Posts With a Social Media API
Scheduling a social media post from code is not a big project. It is one HTTP request. You pick a channel, you write the content, you set a type of schedule and a date in UTC, and you send it. A server owns the clock from there. If your integration is fighting you, the problem is almost never the scheduling. It is authentication, time zones, or the fact that you tried to drive a dashboard that was never built to be driven.
I build a social media scheduling and publishing tool, so I read a lot of code that talks to our API and a lot of code that talks to the raw platform APIs. The pattern is consistent. Teams that treat scheduling as a first-class API call ship in an afternoon. Teams that treat it as an automation problem, screen-scraping a UI or puppeteering a browser, spend weeks and then spend the rest of the year on maintenance. This walkthrough is the first version. Authenticate, create a draft, schedule it, publish now when you need to, and handle status and webhooks so you know what happened.
Every code block below uses the real PostSider Public API. Where I show a shape, it is the shape the API actually accepts, and I link the API docs so you can check the current version yourself.
A typed scheduling call beats scraping a dashboard, every time
A scheduling API is an HTTP endpoint that accepts a post and a future time and takes responsibility for publishing it. You send content plus an ISO 8601 date, and a queue on the server side fires at that moment. You do not keep a process alive, you do not run a cron job on your laptop, and you do not click anything.
The alternative most people reach for first is automating the human dashboard. Do not. Platforms rotate their HTML, block headless browsers, and change selectors on a schedule you do not control, so a scraper breaks regularly and often silently. The research backs this up plainly. As Phyllo’s 2026 social media API guide puts it, scraping “violates most platforms’ terms of service and produces unreliable data,” while official APIs “execute approved, narrow, own-account workloads flawlessly.” Scheduling your own posts is exactly that: a narrow, own-account workload. It is the case APIs are best at.
Here is the difference in one table.
| Concern | Scraping the dashboard | A typed scheduling API |
|---|---|---|
| Stability | Breaks when the HTML changes | Versioned endpoint, stable fields |
| Errors | You guess from a blank screen | Real status codes and a msg body |
| Confirmation | No reliable “did it post” signal | List endpoint plus webhooks |
| Terms of service | Usually a violation | The supported path |
| Auth | Session cookies you have to refresh | One header on every request |
The dry version: if you plan to schedule more than one post, use the API. Scraping is a prototype that becomes a liability the week after you ship it.
Authentication is one header, not a login flow
You authenticate every request with a single credential in the Authorization header. No Bearer prefix. No OAuth dance for your own account. You get the key from Settings, Developers, Public API and you send it on every call.
That is the whole auth story for your own organization. The base URL is https://api.postsider.com/public/v1, and the fastest way to confirm your key works is the connection check.
curl -H "Authorization: your-api-key" \
https://api.postsider.com/public/v1/is-connected
# -> { "connected": true }
PostSider accepts three credential shapes on that same header, and the distinction matters once you move past your own scripts:
- An org API key with no prefix, for your own backend.
- An OAuth token that starts with
pos_, for apps acting on behalf of other users. - An agent token that starts with
agt_, for AI agents and automation pipelines, with its own scopes and rate limit.
You want to store whichever one you use in an environment variable and never commit it. One more thing to file away now: the rate limit is 60 requests per minute per organization, and exceeding it returns 429. That number decides how you write your loops later.
Find the channel first, because a post needs a target
Before you can schedule anything you need the ID of the channel you are posting to. In the PostSider API a connected account is called an integration. In the UI it is called a channel. Same thing, two names, and the naming mismatch trips up everyone once.
List them and grab the ID.
curl -H "Authorization: your-api-key" \
https://api.postsider.com/public/v1/integrations
Each item comes back with an id, a name, an identifier (the platform key, like x or linkedin), and a disabled flag. That id is what you pass as integration.id when you create a post. The disabled flag is worth reading before you schedule: when a channel loses its authorization, disabled goes true, and scheduling to it will not publish. Check it, do not assume it.
In TypeScript with the official SDK, the same call is one line.
import Postsider from '@postsider/node';
const client = new Postsider(process.env.POSTSIDER_API_KEY!);
const channels = await client.integrations();
const x = channels.find((c) => c.identifier === 'x' && !c.disabled);
if (!x) throw new Error('No connected, enabled X channel');
Create a draft before you schedule, so nothing goes out half-written
The single create-post endpoint does four different jobs, and a top-level type field decides which one. draft stores the post without scheduling it. schedule publishes at your date. now publishes immediately. update edits an existing post.
Starting with a draft is the safe default when a human or an agent still needs to review the copy. You create it, it sits against the channel, and nothing publishes until you promote it.
curl -X POST "https://api.postsider.com/public/v1/posts" \
-H "Authorization: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"type": "draft",
"date": "2026-07-01T09:00:00.000Z",
"shortLink": false,
"tags": [],
"posts": [
{
"integration": { "id": "your-integration-id" },
"value": [{ "content": "Draft copy, not live yet.", "image": [] }]
}
]
}'
A few things about that body are load-bearing, and they are the same for every type:
postsis an array. One entry per channel. You can fan out to several channels in one call by adding more entries.valueinside each entry is also an array. Multiple entries chain into a thread or a comment chain, depending on the platform.dateis required even for a draft. It is the stored date the post will use if you later promote it.settingscarries a provider__typeand is required for everything exceptdraft. That is why the draft above can skip it.
Scheduling is the same call with a real time and a provider setting
To schedule, you flip type to schedule, put a UTC ISO 8601 timestamp in date, and add a settings object whose __type names the platform. That __type is the one field people forget, and its absence is the most common 400 I see.
Here is a post scheduled to X for a specific minute.
curl -X POST "https://api.postsider.com/public/v1/posts" \
-H "Authorization: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"type": "schedule",
"date": "2026-07-01T09:00:00.000Z",
"shortLink": false,
"tags": [],
"posts": [
{
"integration": { "id": "your-integration-id" },
"value": [{ "content": "Hello from the PostSider API!", "image": [] }],
"settings": {
"__type": "x",
"who_can_reply_post": "everyone"
}
}
]
}'
The response is an array of created publications, each { "postId": "...", "integration": "..." }. Keep those postId values. They are how you check status, update, or delete later.
The single most common scheduling bug has nothing to do with the API. It is time zones. The date field is UTC ISO 8601, so you convert your local time to UTC before you send it, or your 9am post goes out at some other hour and you spend an afternoon confused. In JavaScript, build the timestamp from a real Date so the conversion is not your problem.
// 9:00 in Europe/Warsaw, converted to a UTC ISO string the API expects
const local = new Date('2026-07-01T09:00:00+02:00');
await client.post({
type: 'schedule',
date: local.toISOString(), // "2026-07-01T07:00:00.000Z"
shortLink: false,
tags: [{ value: 'launch', label: 'Launch' }],
posts: [
{
integration: { id: x.id },
value: [{ content: '<p>Excited to share our new feature.</p>', image: [] }],
settings: { __type: 'x' },
},
],
});
If you do not want to pick the time at all, there is a helper. GET /public/v1/find-slot/:id returns the next open posting slot for a channel as { "date": "..." }, which you can feed straight into a scheduled post. That is how you build a queue without inventing your own slot logic. It also pairs well with a best time to post reference when you want the slot to actually land when people are awake.
One field turns a retry into a duplicate, or not
Networks fail. Your request times out, you did not get a response, and you do not know whether the post was created. If you blindly retry, you double-post. The fix is an idempotency key: a header with a unique string, so a retried request returns the original result instead of creating a second post.
curl -X POST "https://api.postsider.com/public/v1/posts" \
-H "Authorization: your-api-key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: launch-tweet-2026-07-01" \
-d '{ "type": "schedule", "date": "2026-07-01T09:00:00.000Z", "shortLink": false, "tags": [], "posts": [ ... ] }'
Use a UUID, or a hash of the content plus the scheduled time, so the same logical post always carries the same key. This is the difference between an automation you trust and one you babysit. It is especially worth it for agent workflows, where the same request gets retried after a timeout more often than you would like.
Publishing now is the same endpoint with type set to now
Sometimes you do not want a schedule, you want it live this second. Same endpoint, type of now, and the date is ignored.
curl -X POST "https://api.postsider.com/public/v1/posts" \
-H "Authorization: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"type": "now",
"date": "2026-07-01T00:00:00.000Z",
"shortLink": false,
"tags": [],
"posts": [
{
"integration": { "id": "your-integration-id" },
"value": [{ "content": "Publishing this right now.", "image": [] }],
"settings": { "__type": "x" }
}
]
}'
You can also move an existing post between states without recreating it. PUT /public/v1/posts/:id/status with a body of { "status": "schedule" } sets the post to QUEUE and starts the publishing workflow, and { "status": "draft" } pulls it back to DRAFT and stops the workflow so it will not publish. That is your promote and pause switch for a draft you built earlier.
curl -X PUT "https://api.postsider.com/public/v1/posts/your-post-id/status" \
-H "Authorization: your-api-key" \
-H "Content-Type: application/json" \
-d '{ "status": "schedule" }'
Scheduling should be one operation, not one per platform
Here is the founder opinion the rest of this post is built on. Scheduling a post should be a single, first-class API operation, and the API should hide the fact that every network underneath it is different. That is the whole reason a scheduling API exists.
Go the other way and you feel it immediately. Post to X directly and you are on the raw X API, where the free tier now allows 500 posts per calendar month and X has moved new developers onto a pay-per-use model at $0.015 per post created, or $0.20 if the post contains a link, per Postproxy’s 2026 X API pricing breakdown. Then LinkedIn has its own auth, its own review, its own body shape. Then Instagram. Then TikTok. Every platform is a separate integration project with a separate failure mode, and you are the one gluing five schemas together.
A scheduling API collapses that into one call. In the PostSider body, the only thing that changes between platforms is the integration.id and the settings.__type. The type, the date, the value array, the whole envelope stays identical. That is the design goal: you learn the shape once, and adding a platform is a one-line change, not a new project.
If you are still weighing how an agent should reach this same surface, MCP, REST, or an SDK, I wrote a separate piece comparing the three: MCP vs REST vs SDK for social media. The short answer is they hit the same endpoints; they differ in who is calling and how much typing you want. The full endpoint reference lives in the PostSider developer docs.
You confirm a post two ways: poll the list, or receive a webhook
Scheduling a post is not the end of the job. You want to know it actually went out. There are two ways to find out, and they trade latency for simplicity.
The simple way is to poll. GET /public/v1/posts returns posts within a date range, and each post carries its state, so you read the state and decide.
curl -H "Authorization: your-api-key" \
"https://api.postsider.com/public/v1/posts?startDate=2026-07-01T00:00:00.000Z&endDate=2026-07-01T23:59:59.000Z"
Polling is fine for a quick check or a dashboard that refreshes on a timer. It is a poor fit for real-time, and every poll spends one of your 60 requests per minute. Do not poll every second.
The better way, when you can expose a public HTTPS endpoint, is webhooks. PostSider pushes an event to a URL you control the moment something arrives on a source channel, so you never poll. You subscribe once.
curl -X POST "https://api.postsider.com/public/v1/inbound/subscriptions" \
-H "Authorization: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"sources": ["reddit", "gmail"],
"webhookUrl": "https://yourapp.com/webhooks/postsider"
}'
The subscription response includes a secret that starts with whsec_, shown only once. Copy it, store it, because you cannot fetch it again. Every delivery then arrives with an X-Postsider-Signature header, an HMAC-SHA256 of the raw body, and you verify it before you trust the event. The SDK does this for you.
import Postsider from '@postsider/node';
import express from 'express';
const app = express();
// Use raw body parsing so the signature check sees the exact bytes PostSider signed
app.post(
'/webhooks/postsider',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-postsider-signature'] as string;
const rawBody = req.body.toString('utf8');
const secret = process.env.POSTSIDER_WEBHOOK_SECRET!; // your whsec_ value
if (!Postsider.verifyWebhookSignature(signature, rawBody, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
res.status(200).json({ received: true }); // acknowledge fast
const event = JSON.parse(rawBody); // then process async
}
);
Two rules make webhooks reliable. Acknowledge with a 200 before you do any real work, because a slow handler times out and triggers a retry. And make your handler idempotent by deduplicating on the event id, because PostSider retries a failed delivery up to three times and you do not want to process the same event twice. Verify against the raw body string too; if middleware parses the JSON first, the bytes change and the signature check fails. That last one costs people an hour every time.
The same API is what an AI agent drives
The reason I keep calling scheduling a first-class operation is that the moment it is one clean call, something else becomes possible. An agent can make that call. Not a scraper wearing an agent costume, an actual agent holding a scoped agt_ token, hitting the exact POST /public/v1/posts endpoint your backend uses.
That is the bridge. The agent lists channels, drafts copy, finds a slot, schedules the post, and stays inside its own rate limit and scopes the whole time. Same envelope, same type, same date. If you want the agent path specifically, the MCP server wraps this same API so an agent can call named actions instead of raw HTTP. I go deeper on the agent side in driving social media from an AI agent.
If you would rather not build the queue, the retries, the per-platform settings, and the webhook plumbing yourself, that is the tool I make. You can start free on PostSider and schedule your first post over the API in a few minutes, then read the full API reference when you need a field I did not cover here.
Schedule one post over the API today. If it publishes when you said it would and you never opened a dashboard, you built it right.
Frequently asked questions
What is a social media scheduling API?
A social media scheduling API is an HTTP interface that lets your code create a post and set a future publish time, so a server publishes it at that moment instead of a person clicking a button. You send one authenticated request with the content, the target channel, and an ISO 8601 date, and the platform handles the rest.
How do I schedule a post with an API instead of the dashboard?
Get an API key, list your connected channels to find the integration ID, then send a POST request with the post type set to schedule and a date field in UTC ISO 8601. With PostSider you call POST /public/v1/posts with type schedule. The docs at docs.postsider.com/api have the exact body.
Is a scheduling API better than scraping the dashboard?
Yes, for anything you plan to run more than once. A typed API returns a stable response, versioned fields, and real error codes. Scraping a dashboard breaks the moment the HTML changes, violates most terms of service, and gives you no reliable way to confirm a post actually went out.
How do I know if a scheduled post actually published?
Two ways. Poll the list endpoint over a date range and read each post state, or subscribe to inbound webhooks so PostSider pushes events to a URL you control the moment something changes. Webhooks are lower latency and do not burn your rate limit.
What date format do social media scheduling APIs expect?
Almost always UTC in ISO 8601, for example 2026-07-01T09:00:00.000Z. Convert your local time to UTC before you send it. The PostSider date field is UTC ISO 8601, and mixing time zones is the single most common scheduling bug I see.
Can an AI agent schedule posts through the same API?
Yes. The scheduling API is the same surface an agent drives. PostSider exposes scoped agent tokens that start with agt_ and an MCP server, so an agent can create and schedule a post with the same create-post call your backend uses, under its own rate limit and scopes.