How to bulk-schedule posts from a CSV or spreadsheet with the TimeToPost API
TL;DR: To bulk-schedule from a CSV or spreadsheet, map each row to `POST /api/posts` with `content`, target `accountIds` or `platforms`, optional `mediaUrls`, and a future ISO 8601 `scheduledAt`, plus a personal API token from Settings β API. Use `POST /api/media/presign` first for media files, then verify the batch with `GET /api/posts` and the returned lowercase statuses.
What you need before you bulk-schedule from a CSV or spreadsheet
Before you start turning spreadsheet rows into scheduled posts, each row needs the basics TimeToPost understands: post content, target accounts or platforms, optional media URLs, and a future ISO 8601 scheduledAt timestamp. You also need a personal API token from Settings β API, sent as a Bearer token on every request.
A few details are worth keeping in your spreadsheet so the batch does not wander off into the weeds:
content, the text you want posted- either
accountIdsfor specific connected accounts orplatformsfor supported platforms - optional
mediaUrls scheduledAt, and it must be in the future- your API token, kept out of the sheet itself unless you enjoy future security cleanup
Map your spreadsheet columns to the TimeToPost API
The cleanest way to do this is row by row. Treat each spreadsheet record as one post payload, then map columns into the fields that POST /api/posts expects.
A simple mapping might look like this:
post_text->contentaccount_idsorplatforms->accountIdsorplatformsmedia_url_1,media_url_2, and so on ->mediaUrlsscheduled_at->scheduledAt
If your row already has direct media links that TimeToPost can use, pass those through as mediaUrls. If the file is not already in a usable public URL, use POST /api/media/presign first, then upload the file to the returned uploadUrl, and finally reference the returned publicUrl in mediaUrls. That is the documented flow.
For request shape and authentication, the REST API docs are the source of truth. For lifecycle behavior, the scheduling docs confirm that a future scheduledAt creates a scheduled post and omitting it saves a draft.
Worked example: one spreadsheet row becomes one scheduled post
Here is a hypothetical CSV row, clearly labeled as an example:
post_text,platforms,media_url_1,scheduled_at
"Wednesday build notes are out.","linkedin;twitter","https://media.timetopost.co/uploads/build-notes.jpg","2026-07-01T15:00:00.000Z"
A simple mapper could turn that into this JSON payload for POST /api/posts:
{
"content": "Wednesday build notes are out.",
"platforms": ["linkedin", "twitter"],
"mediaUrls": ["https://media.timetopost.co/uploads/build-notes.jpg"],
"scheduledAt": "2026-07-01T15:00:00.000Z"
}
Expected result, based on the docs, would be a 201 response with the created post. Because scheduledAt is in the future, the post is created as scheduled. If you omit scheduledAt, the same endpoint saves a draft instead.
That is the whole trick. One row in, one post out, no wizard robes required.
Bulk-schedule the rows with the REST API
A repeatable batch flow is pretty straightforward:
- Read each CSV or spreadsheet row.
- Validate that
scheduledAtexists and is a future ISO 8601 timestamp. - Map the row into the
POST /api/postspayload. - If the row uses media files, make sure each file has already gone through
POST /api/media/presignand that you are sending the returnedpublicUrl. - Submit the request with your Bearer token.
- Check the response for the created post and its status.
TimeToPost documents lowercase statuses on returned posts, including draft, scheduled, publishing, published, failed, and partial.
Handle common bulk-upload failures before you send the batch
The documented guardrails that matter here are mercifully boring:
- Past
scheduledAtvalues are rejected with a validation error. - Missing or invalid tokens return API errors, and permission or plan limits can also return API errors.
- Media has to go through the presigned upload flow before it is referenced in
mediaUrls.
That means your bulk script should fail fast on bad timestamps, stop trying to schedule with a dead token, and never assume a random file URL can be stuffed straight into a post payload like itβs 2009.
Verify that the batch really scheduled
After the batch runs, list posts with GET /api/posts. The docs say this returns all posts in your active organization, newest first, and each post includes status, scheduledAt, and publishedAt.
What you want to see for scheduled items is the lowercase scheduled status. Depending on timing and workflow, you may also see draft, published, or failed.
A simple verification pass looks like this:
- fetch posts with
GET /api/posts - confirm the expected content is present
- check that the rows you meant to schedule show
status: "scheduled" - check that
scheduledAtmatches the timestamp you sent
If the batch is large, this is the moment where a little bookkeeping pays off. Otherwise you end up scrolling like a raccoon in a spreadsheet.
If you are migrating from another scheduler, what to export first
For this workflow, the only migration artifacts you really need are the spreadsheet or CSV export itself and any media links needed for mediaUrls.
If your export already includes content, targets, and scheduled times, great. If not, make sure you can reconstruct those fields before you try to call the API. Media URLs matter too, because mediaUrls only works when the assets are already uploaded through the documented presigned flow.
Helpful references
- REST API reference, for request shape, authentication, and the
POST /api/postsandPOST /api/media/presignendpoints. - Scheduling docs, for the post lifecycle, lowercase statuses, and the future-
scheduledAtrule.