Bulk import from CSV
Load a spreadsheet of posts
Planning a 30-day product launch? A month of recipes? A quarter of “Tip of the day”? Write them all in a spreadsheet, upload a CSV, and OpenPost schedules the entire campaign.
The CSV shape
Columns are case-insensitive. Only content, channels, and schedule_at are required.
| Field | Type | Description |
|---|---|---|
| content* | string | Base post copy. Use \\n for line breaks inside a cell. |
| channels* | string | Pipe-separated list: x|linkedin|instagram. |
| schedule_at* | ISO 8601 | UTC timestamp. Use 'queue' to drop into the next open slot. |
| media_url | URL | Public image/video URL. We download and attach before scheduling. |
| tags | string | Comma-separated internal tags. |
| alt_text | string | Alt text for the attached media. |
| thread_id | string | Group rows with the same thread_id into a single X thread. |
Example CSV
content,channels,schedule_at,media_url,tags
"Day 1: The idea — 🚀",x|linkedin,2026-05-01T14:00:00Z,https://cdn.example.com/day1.jpg,"launch,day1"
"Day 2: The team behind it",x|linkedin,2026-05-02T14:00:00Z,https://cdn.example.com/day2.jpg,"launch,day2"
"Day 3: First customer result",x|linkedin,2026-05-03T14:00:00Z,,launch
"Day 4: Pricing is live",x|linkedin|instagram,2026-05-04T14:00:00Z,https://cdn.example.com/pricing.png,"launch,pricing"
"Day 5: Thanks for a wild week",x|linkedin,2026-05-05T18:00:00Z,,launch
Upload flow
Publishing → Bulk import
You’ll see a drop zone and an example template link.
Drop in the CSV
The parser runs immediately. Malformed rows are flagged red with the exact reason (missing field, unknown channel, invalid timestamp).
Dry run
Toggle Dry run. OpenPost simulates scheduling every row — checks character limits, media availability, queue collisions — and shows a per-row preview. Fix any issues in the CSV and re-upload.
Confirm and schedule
Hit Schedule. All rows are persisted atomically — either every row lands, or none do. You’ll see them appear on the calendar.
Threads from a CSV
Group rows into an X thread with thread_id. Rows with the same ID become sequential tweets; order is determined by the schedule_at (or by row order if schedule matches).
content,channels,schedule_at,thread_id
"1/ Here's what I learned shipping Open Post for a year 🧵",x,2026-05-01T14:00:00Z,why-op
"2/ Nothing kills momentum like cross-posting by hand.",x,2026-05-01T14:00:00Z,why-op
"3/ So we built this. Simple composer, brutal previews.",x,2026-05-01T14:00:00Z,why-op
"4/ Try it: openpost.so",x,2026-05-01T14:00:00Z,why-op
Queue-aware scheduling
Instead of explicit timestamps, put queue in schedule_at. OpenPost will drop each row into the next open slot for the target channel’s queue, respecting the order in the CSV.
content,channels,schedule_at
"Post one",x,queue
"Post two",x,queue
"Post three",x,queue
With three X slots per week, these would schedule on the next three Tuesdays at your configured time — whatever the queue says.
Bulk import via the API
For programmatic workflows, hit POST /v1/posts/group. Same validation, same atomicity. See the posts API.