How to schedule social media posts with an API
To schedule social media posts with an API, send a future timestamp and channel-specific content to POST /public/v1/posts. On PostSider, the next step matters just as much: save the request and its idempotency key, capture the returned post ID, and read the post back before treating the job as scheduled.
Developer runbook: one text post, one channel, recoverable retries.
In short
- Use
https://api.postsider.com/public/v1with an organization API key in the rawAuthorizationheader.- Save the complete scheduling payload and a unique
Idempotency-Keybefore the first write.- Read the returned post ID back. A create response is not evidence that a social network published it.
This walkthrough uses an already-connected X account because a text-only post keeps the first test small. It is a first-job runbook, not an API comparison or a tour of every publishing mode. For that broader decision, see how to choose a social media scheduling API. The examples below were checked offline against the request DTOs and controller behavior, not executed against a live account.
Start with an account you can identify, not a guessed channel ID
List integrations and explicitly choose the account that should receive the post. The API’s integration.id is a PostSider channel ID, not an X handle or a platform user ID.
Create a PostSider account, connect the intended social media account in the dashboard, and get your organization key from Settings > API. Keep it in your backend environment. Do not put it in a browser bundle or commit it with the script.
The following commands require Bash, curl, jq, and Node.js. Replace the key placeholder locally:
export POSTSIDER_API_BASE='https://api.postsider.com/public/v1'
export POSTSIDER_API_KEY='REPLACE_WITH_YOUR_ORGANIZATION_API_KEY'
curl --fail-with-body --silent --show-error \
-H "Authorization: ${POSTSIDER_API_KEY}" \
"${POSTSIDER_API_BASE}/integrations" \
| jq '.[] | {id, name, identifier, disabled, profile}'
The endpoint returns an array. Find your intended account by name and profile, check that disabled is false, and confirm identifier is x. Do not automatically select the first result: an organization can contain several accounts on the same network.
For this credential type, the header is Authorization: YOUR_KEY, without Bearer. The API overview documents the public URL and organization-key authentication. A working channel-list request establishes that you reached the right API with a valid credential; it does not establish that every later publishing permission is available.
A complete payload prevents avoidable validation failures
Send the scheduling envelope and X’s required reply setting explicitly. The DTO requires shortLink, tags, and each content entry’s image array, even though some abbreviated documentation examples omit them.
Use shortLink: false, tags: [], and image: [] for this text-only job. The posts array holds channel entries; each entry’s value array holds its content. Start with one of each, not a thread or a multi-network launch.
X’s settings include who_can_reply_post. This example sets it to everyone. Other platforms have different settings and media requirements, so changing only the channel ID is not a valid way to reuse this X request everywhere. Read GET /integration-settings/:id for the target channel’s rules and settings schema before adapting it.
Set these placeholders, choosing a future time with an explicit UTC offset:
export POSTSIDER_CHANNEL_ID='REPLACE_WITH_YOUR_X_CHANNEL_ID'
export POSTSIDER_LOCAL_TIME='REPLACE_WITH_FUTURE_ISO_TIME_AND_OFFSET'
# Format example: 2026-11-16T09:00:00-05:00
node <<'NODE'
const fs = require('node:fs');
const { randomUUID } = require('node:crypto');
const channel = process.env.POSTSIDER_CHANNEL_ID;
const local = process.env.POSTSIDER_LOCAL_TIME || '';
if (!channel || channel.startsWith('REPLACE_')) {
throw new Error('Set the chosen X channel ID');
}
if (!/(Z|[+-]\d{2}:\d{2})$/.test(local)) {
throw new Error('Include Z or an explicit UTC offset');
}
const date = new Date(local);
if (!Number.isFinite(date.getTime()) || date.getTime() <= Date.now()) {
throw new Error('Choose a valid future time');
}
const payload = {
type: 'schedule',
date: date.toISOString(),
shortLink: false,
tags: [],
posts: [{
integration: { id: channel },
value: [{ content: 'A text-only scheduling test from our API.', image: [] }],
settings: { __type: 'x', who_can_reply_post: 'everyone' }
}]
};
fs.writeFileSync('schedule-request.json', JSON.stringify(payload), { flag: 'wx' });
fs.writeFileSync('schedule-key.txt', randomUUID(), { flag: 'wx' });
console.log('Scheduled UTC time:', payload.date);
NODE
Run this preparation once in a dedicated job directory. Exclusive file creation stops an accidental rerun from replacing an existing payload. If preparation fails halfway through, inspect the files before sending anything.
The offset is essential. A bare local date leaves timezone interpretation to the machine running the script. Converting to toISOString() produces UTC with Z. Review that printed time against the intended local appointment, including daylight saving rules. Do not recalculate it on a retry.
The posts reference describes the scheduling endpoint. Here, the example includes the stricter fields found in the application DTO rather than depending on omitted-field defaults.
Save the response, then verify the exact post
Submit the saved JSON with the saved key, then query the returned post ID. This separates request acceptance from what the scheduling system actually stored.
KEY="$(<schedule-key.txt)"
curl --silent --show-error --max-time 30 \
-D schedule-response.headers \
-o schedule-response.json \
-w 'HTTP %{http_code}\n' \
-X POST "${POSTSIDER_API_BASE}/posts" \
-H "Authorization: ${POSTSIDER_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${KEY}" \
--data-binary @schedule-request.json
Check both curl’s exit status and the printed HTTP status. Without --fail, curl can exit successfully for an HTTP error; that is why this command preserves the body for inspection. A successful create response is an array with postId and integration in each entry, not a full post object.
Only after a successful HTTP response, extract the ID and read it:
POST_ID="$(jq -er '.[0].postId' schedule-response.json)" || exit 1
curl --fail-with-body --silent --show-error \
-H "Authorization: ${POSTSIDER_API_KEY}" \
"${POSTSIDER_API_BASE}/posts/${POST_ID}" \
| jq '{group, integration, settings,
posts: [.posts[] | {id, state, publishDate, content, releaseURL}]}'
The detail response wraps records in posts, alongside group, integration, and settings. Compare the returned integration with your chosen channel and publishDate with the saved request date. Confirm the content, settings, and QUEUE state. Keep the post ID with your job record.
Scheduling starts asynchronously. A later scheduling failure can mark the record ERROR, so even an immediate successful readback is not a permanent delivery guarantee. Check again after the scheduled time; investigate errors rather than creating a replacement automatically. When a publication URL is available, inspect it on the destination network.
Safe retries preserve the original job, not just its text
After a timeout, replay the same payload with the same key. Never generate a fresh UUID merely because the response was lost.
For a completed matching request, PostSider can return the saved result. Its implementation hashes JSON.stringify of the parsed request body, so preserve property order too: equivalent content reconstructed in a different order can produce a different hash. Sending the saved file avoids that mistake.
Before rerunning the create command, archive any response files you want to retain; that command overwrites them. Use a bounded retry policy:
| Result | Next action |
|---|---|
| Timeout, connection failure, or 5xx | Wait, then retry the saved request and key; stop after a bounded number of attempts. |
| 429 | Wait for the Retry-After interval before retrying. |
| 400 | Inspect the validation message. Fix the request deliberately; do not loop. |
| 401 or 402 | Resolve authentication or account access before another write. |
| 409 | Read the message: invalid key, changed request, and still-processing request need different responses. |
An in-progress key returns a conflict rather than a completed replay. Wait before checking again. A changed-payload conflict means this is no longer the original operation; reconcile any existing post before deciding whether to create a separate job.
Idempotency reduces duplicate creation on retries. It does not guarantee exactly-once publication on every social network, and this runbook should not treat it as that promise.
Finish the first job before expanding the automation
A useful first milestone is a saved request linked to a verified post, followed by a publication check. Only then add more channels or media.
If your pipeline loses the response and cannot recover an ID, GET /posts accepts startDate and endDate and returns {posts: [...]}. Search a narrow UTC range around the saved date, then compare channel and content. Do not identify a job by text alone or create another post just because one list query came back empty.
For queue-based scheduling, GET /find-slot/:id returns {date}. Resolve that slot once and persist it before creating the job; asking for a new slot during retry changes the payload. For the broader create, draft, and publish flow, see scheduling posts with a social media API.
Keep the same post visible in the dashboard so a human can inspect a failure or correct the schedule. Start with one connected account, save one request, and prove its readback before putting the workflow on a timer.
Lukasz Blania is the founder of PostSider, a social media scheduling product with a dashboard and API access for developer workflows.
Frequently asked questions
Do I put Bearer before my PostSider API key?
No. For an organization API key, send the raw key in the Authorization header. This tutorial uses that credential type, not an OAuth access token.
Which date format should I send for a scheduled post?
Send an ISO 8601 timestamp. Convert an explicit local offset to UTC and send the resulting timestamp ending in Z. Choose a future time and keep that date unchanged during retries.
Can I retry a create request after a timeout?
Retry with the original Idempotency-Key and unchanged JSON payload. A completed matching request can return its saved result. A 409 can mean an invalid key, a changed payload, or an original request still processing; inspect the message before acting.
Does a successful create response mean the post is published?
No. The response provides post IDs. Read each post back to check its scheduled time and state, then check again after the publishing time. QUEUE is scheduled work, not proof of publication.