Got 200 posts to schedule? Don’t loop POST /post like a maniac. Upload a CSV and let us handle it.
Each row in the CSV becomes one post. Multiple platforms per row, multiple teams, media referenced by URL - one bad row won’t block the rest.
How it works
This is an asynchronous job, same pattern as Import Post History.
- Upload: You send us a CSV file.
- Wait: We process each row in the background.
- Poll: You check if we’re done.
- Results: You get per-row success/failure details.
Step 1: Upload the CSV
Endpoint: POST /api/v1/post-csv-import
curl -X POST "https://api.bundle.social/api/v1/post-csv-import" \
-H "x-api-key: YOUR_KEY" \
-F "file=@./posts.csv"
The file must be .csv and under 100 MB.
Response (201):
{
"id": "import_abc123",
"teamId": "team_456",
"organizationId": "org_789",
"status": "PENDING",
"fileName": "posts.csv",
"totalRows": 200,
"processedRows": 0,
"successRows": 0,
"failedRows": 0,
"error": null,
"rateLimitResetAt": null,
"startedAt": null,
"completedAt": null,
"createdAt": "2026-03-31T12:00:00.000Z",
"updatedAt": "2026-03-31T12:00:00.000Z"
}
Every row in the CSV must include a teamId, and all referenced teams must belong to the same organization that owns your API key.
Step 2: Poll Progress
You have two options: lightweight status or full import details.
Status (lightweight)
Endpoint: GET /api/v1/post-csv-import/{importId}/status
curl "https://api.bundle.social/api/v1/post-csv-import/import_abc123/status" \
-H "x-api-key: YOUR_KEY"
{
"id": "import_abc123",
"status": "PROCESSING",
"totalRows": 200,
"processedRows": 87,
"successRows": 85,
"failedRows": 2,
"error": null,
"startedAt": "2026-03-31T12:00:01.000Z",
"completedAt": null,
"updatedAt": "2026-03-31T12:00:45.000Z"
}
Full import details
Endpoint: GET /api/v1/post-csv-import/{importId}
Returns the full import object (same shape as the POST response).
Import history
Endpoint: GET /api/v1/post-csv-import
| Parameter | Type | Default | Description |
|---|
offset | number | 0 | Skip this many records |
limit | number | 10 | How many to return |
Returns { items, total }, newest first.
Step 3: Row Results
Once the import finishes (or while it’s running), you can inspect individual rows.
Endpoint: GET /api/v1/post-csv-import/{importId}/rows
| Parameter | Type | Default | Description |
|---|
status | string | - | Filter by SUCCESS or FAILED |
offset | number | 0 | Skip this many rows |
limit | number | 10 | How many to return |
curl "https://api.bundle.social/api/v1/post-csv-import/import_abc123/rows?status=FAILED" \
-H "x-api-key: YOUR_KEY"
{
"items": [
{
"id": "row_001",
"importId": "import_abc123",
"rowNumber": 14,
"status": "FAILED",
"postId": null,
"rawRow": {
"teamId": "team_456",
"postDate": "2026-04-05T10:00:00Z",
"socialAccountTypes": "INSTAGRAM,TIKTOK",
"text": "Hello world!"
},
"normalizedPayload": null,
"error": "Team not found or does not belong to your organization",
"createdAt": "2026-03-31T12:00:12.000Z"
}
],
"total": 2
}
Successful rows include the postId so you can track the created post:
{
"rowNumber": 1,
"status": "SUCCESS",
"postId": "post_xyz789",
"error": null
}
Import Statuses
| Status | Meaning |
|---|
PENDING | Queued, processing hasn’t started yet. |
PROCESSING | We’re working through the rows. |
COMPLETED | All rows processed successfully. |
COMPLETED_WITH_ERRORS | Done, but at least one row failed. |
FAILED | The entire import failed (e.g. invalid file). |
RATE_LIMITED | Paused due to platform rate limits. |
Each row is one post. Column names map to post fields:
| Column | Required | Description |
|---|
teamId | Yes | Which team this post belongs to |
socialAccountTypes | Yes | Comma-separated platforms (e.g. INSTAGRAM,TIKTOK,LINKEDIN) |
text | - | Post body text |
postDate | - | ISO 8601 date for scheduling (omit for immediate) |
*MediaUrls | - | Platform-specific media URLs (e.g. instagramMediaUrls, tiktokMediaUrls) |
Media is referenced by URL - we download and process it for you. No need to upload files first with /upload.
Download a CSV template from the bundle.social dashboard under the Bulk Post section to see all available columns and example values.
Handling Rate Limits
If the import status goes to RATE_LIMITED, don’t panic. We hit a platform’s posting cap.
- The import pauses automatically.
- Check
rateLimitResetAt for when it can resume.
- We retry automatically - just keep polling.
This works the same as Import Post History rate limiting.
See also
- Import Post History - same async pattern, but for pulling existing posts from platforms.
- Media Upload - if you prefer uploading media separately before creating posts.
- Rate Limits - posting limits per platform and plan.