Skip to main content

Documentation Index

Fetch the complete documentation index at: https://info.bundle.social/llms.txt

Use this file to discover all available pages before exploring further.

Got 200 posts to schedule? Upload a CSV instead of creating every post with a separate POST /post request. 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.
  1. Upload: You send us a CSV file.
  2. Wait: We process each row in the background.
  3. Poll: You check if we’re done.
  4. 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
ParameterTypeDefaultDescription
offsetnumber0Skip this many records
limitnumber10How 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
ParameterTypeDefaultDescription
statusstring-Filter by SUCCESS or FAILED
offsetnumber0Skip this many rows
limitnumber10How 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

StatusMeaning
PENDINGQueued, processing hasn’t started yet.
PROCESSINGWe’re working through the rows.
COMPLETEDAll rows processed successfully.
COMPLETED_WITH_ERRORSDone, but at least one row failed.
FAILEDThe entire import failed (e.g. invalid file).
RATE_LIMITEDPaused due to platform rate limits.

CSV Format

Each row is one post. Column names map to post fields:
ColumnRequiredDescription
teamIdYesWhich team this post belongs to
titleYesInternal post title shown in bundle.social
postDateYesISO 8601 date-time with timezone, for example 2026-04-05T10:00:00Z
statusYesDRAFT or SCHEDULED
socialAccountTypesYesComma-separated platforms (e.g. INSTAGRAM,TIKTOK,LINKEDIN)
text-Post body text
*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. Use # to separate multiple media URLs or list-like values inside one cell:
instagramMediaUrls
https://cdn.example.com/1.jpg#https://cdn.example.com/2.jpg

Platform Media URL Columns

PlatformColumn
FacebookfacebookMediaUrls
InstagraminstagramMediaUrls
TikToktiktokMediaUrls
YouTubeyoutubeMediaUrls
LinkedInlinkedinMediaUrls
PinterestpinterestMediaUrls
RedditredditMediaUrls
ThreadsthreadsMediaUrls
Twitter / XtwitterMediaUrls
MastodonmastodonMediaUrls
BlueskyblueskyMediaUrls
DiscorddiscordMediaUrls
SlackslackMediaUrls
Google BusinessgoogleBusinessMediaUrls

Common Platform Columns

PlatformColumns
FacebookfacebookType, facebookText, facebookLink, facebookThumbnail, facebookMediaTitle, facebookNativeScheduleTime
InstagraminstagramType, instagramText, instagramThumbnailOffset, instagramThumbnail, instagramShareToFeed, instagramCollaborators, instagramAutoFitImage, instagramAutoCropImage, instagramTagged, instagramCarouselItems, instagramLocationId, instagramTrialParamsGraduationStrategy, instagramMusicSoundId, instagramMusicSoundVolume, instagramVideoOriginalSoundVolume
TikToktiktokType, tiktokText, tiktokThumbnail, tiktokPrivacy, tiktokIsBrandContent, tiktokIsOrganicBrandContent, tiktokDisableComments, tiktokDisableDuet, tiktokDisableStitch, tiktokThumbnailOffset, tiktokIsAiGenerated, tiktokAutoAddMusic, tiktokAutoScale, tiktokUploadToDraft, tiktokPhotoCoverIndex, tiktokMusicSoundId, tiktokMusicSoundVolume, tiktokMusicSoundStart, tiktokMusicSoundEnd, tiktokVideoOriginalSoundVolume
YouTubeyoutubeType, youtubeTitle, youtubeDescription, youtubeThumbnail, youtubePrivacy, youtubeMadeForKids, youtubeContainsSyntheticMedia, youtubeHasPaidProductPlacement
LinkedInlinkedinText, linkedinLink, linkedinThumbnail, linkedinMediaTitle, linkedinPrivacy, linkedinHideFromFeed, linkedinDisableReshare
PinterestpinterestBoardName, pinterestTitle, pinterestDescription, pinterestLink, pinterestThumbnail, pinterestAltText, pinterestNote, pinterestDominantColor
RedditredditSubreddit, redditText, redditDescription, redditLink, redditNsfw, redditFlairId
ThreadsthreadsText
Twitter / XtwitterText, twitterReplySettings
MastodonmastodonText, mastodonPrivacy, mastodonThumbnail, mastodonSpoiler
BlueskyblueskyText, blueskyTags, blueskyLabels, blueskyQuoteUri, blueskyExternalUrl, blueskyExternalTitle, blueskyExternalDescription, blueskyVideoAlt
DiscorddiscordChannelId, discordText, discordUsername, discordAvatarUrl
SlackslackChannelId, slackText, slackUsername, slackAvatarUrl
Google BusinessgoogleBusinessText, googleBusinessTopicType, googleBusinessLanguageCode, googleBusinessCallToActionType, googleBusinessCallToActionUrl, googleBusinessEventTitle, googleBusinessEventStartDate, googleBusinessEventEndDate, googleBusinessOfferCouponCode, googleBusinessOfferRedeemOnlineUrl, googleBusinessOfferTermsConditions, googleBusinessAlertType
Boolean columns must be TRUE or FALSE. Enum-like columns are case-insensitive in practice because we normalize them to uppercase before validation.
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.