Publishing immediately is useful for testing. In real life, most posts are planned: a launch at 9:00 in New York, a weekly digest every Friday morning, a campaign that goes out across eight accounts at the same moment.
Relaycast handles all of these with one field. This tutorial shows how scheduling works, how to get time zones right, and how to keep an eye on what is queued.
The short version
Replace publishNow: true with scheduledFor, an ISO 8601 timestamp that includes a time zone offset:
curl -X POST "$RELAYCAST_API_URL/v1/posts" \
-H "x-access-key: $RELAYCAST_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: spring-launch-2026" \
-d '{
"content": "Our spring collection is here.",
"media": [{ "url": "https://cdn.example.com/spring.jpg", "type": "image", "altText": "Three jackets on a rack" }],
"targets": [
{ "accountId": "INSTAGRAM_ACCOUNT_ID" },
{ "accountId": "FACEBOOK_ACCOUNT_ID" },
{ "accountId": "THREADS_ACCOUNT_ID" },
{ "accountId": "PINTEREST_ACCOUNT_ID", "options": { "boardId": "BOARD_ID" } }
],
"scheduledFor": "2026-05-12T09:00:00-04:00"
}'await fetch(`${process.env.RELAYCAST_API_URL}/v1/posts`, {
method: "POST",
headers: {
"x-access-key": process.env.RELAYCAST_API_KEY!,
"Content-Type": "application/json",
"Idempotency-Key": "spring-launch-2026",
},
body: JSON.stringify({
content: "Our spring collection is here.",
media: [{ url: "https://cdn.example.com/spring.jpg", type: "image", altText: "Three jackets on a rack" }],
targets: [
{ accountId: "INSTAGRAM_ACCOUNT_ID" },
{ accountId: "FACEBOOK_ACCOUNT_ID" },
{ accountId: "THREADS_ACCOUNT_ID" },
{ accountId: "PINTEREST_ACCOUNT_ID", options: { boardId: "BOARD_ID" } },
],
scheduledFor: "2026-05-12T09:00:00-04:00",
}),
});import os, requests
requests.post(
f"{os.environ['RELAYCAST_API_URL']}/v1/posts",
headers={"x-access-key": os.environ["RELAYCAST_API_KEY"], "Idempotency-Key": "spring-launch-2026"},
json={
"content": "Our spring collection is here.",
"media": [{"url": "https://cdn.example.com/spring.jpg", "type": "image", "altText": "Three jackets on a rack"}],
"targets": [
{"accountId": "INSTAGRAM_ACCOUNT_ID"},
{"accountId": "FACEBOOK_ACCOUNT_ID"},
{"accountId": "THREADS_ACCOUNT_ID"},
{"accountId": "PINTEREST_ACCOUNT_ID", "options": {"boardId": "BOARD_ID"}},
],
"scheduledFor": "2026-05-12T09:00:00-04:00",
},
)The API answers 201 Created with a post whose status is scheduled. At the scheduled time, every target is published by its own job, in parallel.
A single request can target up to 20 accounts.
What happens when you schedule
Scheduling does the same work up front as publishing immediately. That is a feature: you find out about problems now, not at 9:00 on launch day.
- Validation runs immediately. Each target is checked against its platform's rules. A caption that is too long for one network, or an Instagram post with no media, returns a
400 validation_errornow. - Accounts must be active. If one of the accounts needs reconnecting, the request is rejected with
account_inactive. - Credits are reserved. One credit per target is debited when the post is created. Targets that end up failing are refunded.
- The time must be in the future. A
scheduledFormore than a minute in the past is rejected.
Exactly one of publishNow or scheduledFor is needed. If you send neither, the request fails validation.
Getting time zones right
scheduledFor must include an offset: either Z for UTC or an explicit +hh:mm / -hh:mm. A timestamp without one, such as 2026-05-12T09:00:00, is rejected, because it is ambiguous.
These three values are the same instant:
2026-05-12T13:00:00Z
2026-05-12T09:00:00-04:00
2026-05-12T15:00:00+02:00The common mistake is computing the offset once and reusing it. Offsets change with daylight saving time, so "9:00 in New York" is -05:00 in January and -04:00 in May. Let a time zone library do the conversion for the specific date:
// Using Luxon: 9:00 local time in New York on a given date.
import { DateTime } from "luxon";
const scheduledFor = DateTime.fromObject(
{ year: 2026, month: 5, day: 12, hour: 9 },
{ zone: "America/New_York" },
).toISO(); // "2026-05-12T09:00:00.000-04:00"from datetime import datetime
from zoneinfo import ZoneInfo
scheduled_for = datetime(2026, 5, 12, 9, 0, tzinfo=ZoneInfo("America/New_York")).isoformat()
# "2026-05-12T09:00:00-04:00"Store the user's chosen time zone name (like Europe/Berlin) alongside their schedule, not a numeric offset. Convert to an offset only when you create the post.
Same moment, different words
Scheduling one post for many accounts does not mean the same text everywhere. Set content on a target to replace the shared text for that account, and options for platform settings:
{
"content": "Our spring collection is here. Shop now at example.com/spring",
"targets": [
{ "accountId": "LINKEDIN_ACCOUNT_ID", "content": "Behind the scenes of designing our spring collection: example.com/blog/spring" },
{ "accountId": "BLUESKY_ACCOUNT_ID" },
{ "accountId": "INSTAGRAM_ACCOUNT_ID", "content": "Spring is here. Link in bio." }
],
"scheduledFor": "2026-05-12T13:00:00Z"
}Different times for different accounts
One request has one scheduledFor. If you want each region to get the post at its own local morning, send one request per time slot, each with its own targets and its own idempotency key:
const slots = [
{ zone: "America/New_York", accounts: ["US_INSTAGRAM_ID", "US_FACEBOOK_ID"] },
{ zone: "Europe/London", accounts: ["UK_INSTAGRAM_ID", "UK_FACEBOOK_ID"] },
];
for (const slot of slots) {
const scheduledFor = DateTime.fromObject({ year: 2026, month: 5, day: 12, hour: 9 }, { zone: slot.zone }).toISO();
await createPost(
{ content: "Good morning! Our spring collection is here.", targets: slot.accounts.map((accountId) => ({ accountId })), scheduledFor },
`spring-launch-${slot.zone}`,
);
}Keeping track of what is queued
GET /v1/postslists recent posts (newest first, 20 by default, up to 100 with?limit=). Scheduled posts havestatus: "scheduled".GET /v1/posts/{id}returns one post with every target.- The Posts page in the dashboard shows scheduled posts alongside published ones, and lets you delete a scheduled post before it goes out.
To be told when things happen rather than checking, add a rule under Settings → Notifications. The post.scheduled event fires when a scheduled post is created, and post.published, post.partial and post.failed fire when it finishes. There is also an option to only notify for scheduled posts, skipping ones published immediately. See notifications.
Scheduling from the dashboard
The Playground in the dashboard has a schedule picker, so non-developers on your team can plan posts too. It uses the same validation and the same request format, and shows you the exact request body it will send.
More detail is in the scheduling guide and the create post reference.
Ready to build?
Get an access key and publish your first post in minutes.