Skill: tarasshyn/adaptlypost (version 1.0.5)

--- name: adaptlypost description: Schedule and manage social media posts across Instagram, X (Twitter), Bluesky, TikTok, Threads, LinkedIn, Facebook, Pinterest, and YouTube using the AdaptlyPost API. Use when the user wants to schedule social media posts, manage social media content, upload media for social posting, list connected social accounts, check post status, cross-post content to multiple platforms, or automate their social media workflow. AdaptlyPost is a SaaS tool — no self-hosting required. homepage: https://adaptlypost.com metadata: { 'openclaw': { 'emoji': '📬', 'primaryEnv': 'ADAPTLYPOST_API_KEY', 'requires': { 'env': ['ADAPTLYPOST_API_KEY'] } } } --- # AdaptlyPost Schedule social media posts across 9 platforms from one API. SaaS — no self-hosting needed. ## Setup 1. Sign up at https://adaptlypost.com/signup 2. Go to Settings → API Tokens → generate a **dedicated, revocable** API token for this agent — do not reuse a token that is also used by other tools or humans. 3. Connect only the social accounts the agent actually needs. The token has delegated access to every account in the group, so a smaller group = smaller blast radius. 4. Set the environment variable: ```bash export ADAPTLYPOST_API_KEY="adaptly_your-token-here" ``` Base URL: `https://post.adaptlypost.com/post/api/v1` Auth header: `Authorization: Bearer $ADAPTLYPOST_API_KEY` ## Safety rules — read before any write call Posts are **public, attributable, and hard to fully retract**. Treat every `POST /social-posts` and `POST /upload-urls` as a high-impact action. 1. **Confirm before every post.** Before calling `POST /social-posts`, show the user a summary and get an explicit "yes" covering all four items: - **Content** — exact text (and per-platform overrides), media filenames - **Platforms** — which networks and which connected accounts (by `displayName`/`username`, not just ID) - **Timing** — "now", a specific scheduled time, or draft - **Visibility** — TikTok `privacyLevel`, YouTube `privacyStatus`, Instagram `postType`, etc. A previous "yes" does not authorize a new post. Re-confirm each one. 2. **Prefer drafts when uncertain.** If the user has not run this skill before, or the content is sensitive, default to `saveAsDraft: true` and let them review in the AdaptlyPost UI before publishing. 3. **Never batch without explicit batch consent.** If the user asks to schedule many posts in a row, ask them to confirm a **small first batch** (e.g. 1–3 posts) before scheduling the rest. A single typo or wrong connection ID will otherwise propagate to every queued post. 4. **Verify media before upload.** Files uploaded via `/upload-urls` are stored at a **public URL** that exists from the moment of upload — before the post goes live, and even if the post is never created. Before calling `/upload-urls`: - Confirm the exact file path with the user. - Refuse to upload files from directories that may contain unrelated content (`~/Downloads`, `~/Desktop`, screenshot folders, etc.) without an explicit per-file "yes". - Never upload a file the user did not name. 5. **Do not retry failed posts silently.** If a `POST /social-posts` returns an error or unexpected `skippedPlatforms`, surface it to the user and ask before retrying — do not loop. ## Core Workflow ### 1. List connected accounts ```bash curl -s -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ https://post.adaptlypost.com/post/api/v1/social-accounts ``` Returns `{ "accounts": [{ "id", "platform", "displayName", "username", "avatarUrl" }] }`. Save the `id` — you'll use it as a connection ID when creating posts. ### 2. Publish a post immediately (no scheduling) ⚠️ **Immediate publish is irreversible from the agent's side** — once `POST /social-posts` returns, the content is live on the user's connected accounts. Only call this after the four-item confirmation in [Safety rules](#safety-rules--read-before-any-write-call). To publish right away, simply **omit `scheduledAt` entirely** and do NOT set `saveAsDraft`: ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/social-posts \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platforms": ["TWITTER"], "contentType": "TEXT", "text": "This goes live right now!", "timezone": "America/New_York", "twitterConnectionIds": ["CONNECTION_ID_HERE"] }' ``` **IMPORTANT**: Do NOT set `scheduledAt` to a time in the near future as a workaround. Omitting `scheduledAt` is the correct way to publish immediately. Returns `{ "postId", "queuedPlatforms", "skippedPlatforms", "isScheduled", "scheduledAt" }`. **Important**: You must include the correct `*ConnectionIds` array for each platform in `platforms`. For example, if posting to Instagram and Twitter, include both `instagramConnectionIds` and `twitterConnectionIds`. For Facebook, use `pageIds` instead. ### 3. Schedule a text post for later ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/social-posts \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platforms": ["TWITTER"], "contentType": "TEXT", "text": "Your post text here", "timezone": "America/New_York", "scheduledAt": "2026-06-15T10:00:00.000Z", "twitterConnectionIds": ["CONNECTION_ID_HERE"] }' ``` ### 4. Save a post as draft (no scheduling) Same as scheduling, but set `saveAsDraft: true` and omit `scheduledAt`: ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/social-posts \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platforms": ["INSTAGRAM"], "contentType": "TEXT", "text": "Draft post to review later", "timezone": "Europe/London", "saveAsDraft": true, "instagramConnectionIds": ["CONNECTION_ID_HERE"] }' ``` ### 5. Schedule a post with media (3-step flow) ⚠️ **The `publicUrl` returned in Step A is publicly reachable as soon as Step B completes** — even if you never create the post in Step C. Confirm the exact file path with the user before Step A, and never upload a file the user has not explicitly named. **Step A** — Get presigned upload URLs: ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/upload-urls \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "files": [{ "fileName": "photo.jpg", "mimeType": "image/jpeg" }] }' ``` Returns `{ "urls": [{ "fileName", "uploadUrl", "publicUrl", "key", "expiresAt" }] }`. **Step B** — Upload file to storage: ```bash curl -X PUT "UPLOAD_URL_HERE" \ -H "Content-Type: image/jpeg" \ --data-binary @/path/to/photo.jpg ``` **Step C** — Create post with the public URL: ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/social-posts \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platforms": ["INSTAGRAM"], "contentType": "IMAGE", "text": "Post with image!", "mediaUrls": ["PUBLIC_URL_FROM_STEP_A"], "timezone": "America/New_York", "scheduledAt": "2026-06-15T10:00:00.000Z", "instagramConnectionIds": ["CONNECTION_ID_HERE"] }' ``` For video: use `mimeType: "video/mp4"`, `contentType: "VIDEO"`. For carousel: upload multiple files, include all public URLs in `mediaUrls`, use `contentType: "CAROUSEL"`. ### 6. List posts ```bash curl -s -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ "https://post.adaptlypost.com/post/api/v1/social-posts?limit=20&offset=0" ``` Returns `{ "posts": [...], "total": 25, "hasMore": true }`. Use `limit` (1-100, default 20) and `offset` (default 0) for pagination. ### 7. Get post details ```bash curl -s -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ https://post.adaptlypost.com/post/api/v1/social-posts/POST_ID ``` Returns full post object with platform-specific status for each target platform. ### 8. Cross-post to multiple platforms Include multiple platforms and their connection IDs in a single request: ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/social-posts \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platforms": ["TWITTER", "BLUESKY", "LINKEDIN"], "contentType": "TEXT", "text": "Same post across 3 platforms!", "timezone": "America/New_York", "scheduledAt": "2026-06-15T10:00:00.000Z", "twitterConnectionIds": ["TWITTER_ID"], "blueskyConnectionIds": ["BLUESKY_ID"], "linkedinConnectionIds": ["LINKEDIN_ID"] }' ``` ### 9. Use per-platform text Override the default text for specific platforms: ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/social-posts \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platforms": ["TWITTER", "LINKEDIN"], "contentType": "TEXT", "text": "Default text for all platforms", "platformTexts": [ { "platform": "TWITTER", "text": "Short version for X #shortform" }, { "platform": "LINKEDIN", "text": "Longer professional version with more detail for LinkedIn audience." } ], "timezone": "America/New_York", "scheduledAt": "2026-06-15T10:00:00.000Z", "twitterConnectionIds": ["TWITTER_ID"], "linkedinConnectionIds": ["LINKEDIN_ID"] }' ``` ## Platform-Specific Configs Pass these as config arrays in the request body. See [references/platform-configs.md](references/platform-configs.md) for full details. | Platform | Config Field | Key Options | | --------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | | **TikTok** | `tiktokConfigs` | `privacyLevel` (required), `allowComments`, `allowDuet`, `allowStitch`, `sendAsDraft`, `brandedContent`, `autoAddMusic` | | **Instagram** | `instagramConfigs` | `postType` (FEED/REEL/STORY) | | **Facebook** | `facebookConfigs` | `postType` (FEED/REEL/STORY), `videoTitle` | | **YouTube** | `youtubeConfigs` | `postType` (VIDEO/SHORTS), `videoTitle`, `tags`, `privacyStatus`, `madeForKids`, `playlistId` | | **Pinterest** | `pinterestConfigs` | `boardId` (required), `title`, `link` | | **X (Twitter)** | — | No config object, uses `twitterConnectionIds` only | | **Bluesky** | — | No config object, uses `blueskyConnectionIds` only | | **Threads** | — | No config object, uses `threadsConnectionIds` only | | **LinkedIn** | — | No config object, uses `linkedinConnectionIds` only | **Example with TikTok config:** ```bash curl -X POST https://post.adaptlypost.com/post/api/v1/social-posts \ -H "Authorization: Bearer $ADAPTLYPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platforms": ["TIKTOK"], "contentType": "VIDEO", "text": "Check out this clip!", "mediaUrls": ["https://cdn.adaptlypost.com/social-media-posts/uuid/video.mp4"], "timezone": "America/New_York", "scheduledAt": "2026-06-15T18:00:00.000Z", "tiktokConnectionIds": ["TIKTOK_ID"], "tiktokConfigs": [{ "connectionId": "TIKTOK_ID", "privacyLevel": "PUBLIC_TO_EVERYONE", "allowComments": true, "allowDuet": false, "allowStitch": true }] }' ``` ## Supported File Types for Upload | MIME Type | Extension | Use For | | ----------------- | ----------- | ------- | | `image/jpeg` | .jpg, .jpeg | Images | | `image/png` | .png | Images | | `image/webp` | .webp | Images | | `video/mp4` | .mp4 | Videos | | `video/quicktime` | .mov | Videos | Upload 1-20 files per request. ## Media Specs Quick Reference | Platform | Images | Video | Carousel | | ----------- | --------------- | ------------------------- | --------------- | | TikTok | Carousels only | MP4/MOV, ≤250MB, 3s-10min | 2-35 images | | Instagram | JPEG/PNG | ≤1GB, 3-90s (Reels) | Up to 10 | | Facebook | ≤30MB, JPG/PNG | 1 per post | Up to 10 images | | YouTube | — | Shorts ≤3min, H.264 | — | | LinkedIn | Up to 9 | ≤10min | Up to 9 | | X (Twitter) | Up to 4 | — | — | | Pinterest | 2:3 ratio ideal | Supported | 2-5 images | | Bluesky | Up to 4 | Not supported | — | | Threads | Supported | Supported | Up to 10 | ## Tips for the Agent ### CRITICAL — Always ask before posting - **NEVER assume** whether the user wants to post now, schedule for later, or save as draft. **ALWAYS ask** the user: "Do you want to post this now, schedule it for a specific time, or save it as a draft?" Wait for their answer before making the API call. - Before calling `POST /social-posts`, show a final summary covering **content, platforms (named, not just IDs), timing, and visibility**, and wait for an explicit "yes". Re-confirm for every post — prior approval does not carry over. - When in doubt, **prefer `saveAsDraft: true`** so the user can review in the AdaptlyPost UI before anything goes live. - For multi-post sessions, schedule a **small first batch (1–3)** and confirm before queuing the rest. A single mistake otherwise propagates across every queued post. - If the user says "post now", "publish now", or "right away": **completely omit `scheduledAt` from the request body** — do NOT set it to a time in the near future. The API publishes immediately when `scheduledAt` is absent. - If the user says "schedule": ask for the date and time, then set `scheduledAt` to an ISO 8601 timestamp. - If the user says "draft": set `saveAsDraft: true` and omit `scheduledAt`. ### Timezone handling - The `timezone` field is **required** on every post creation request. - **On the first interaction**, ask the user: "What timezone are you in? (e.g., Europe/Berlin, America/New_York)". Once they answer, **remember it for all future posts** in this conversation — do not ask again. - If the user has previously told you their timezone in this conversation, reuse it silently. - Common timezones: `Europe/London`, `Europe/Berlin`, `Europe/Paris`, `America/New_York`, `America/Chicago`, `America/Los_Angeles`, `Asia/Tokyo`, `Australia/Sydney`. ### API workflow - Always call `/social-accounts` first to get valid connection IDs for each platform. - For media posts, complete the full 3-step upload flow (get upload URL → PUT file → create post with `mediaUrls`). - `scheduledAt` must be ISO 8601 and in the future. Omit it when using `saveAsDraft: true`. - Each platform needs its connection IDs: `twitterConnectionIds`, `instagramConnectionIds`, `blueskyConnectionIds`, `linkedinConnectionIds`, `tiktokConnectionIds`, `threadsConnectionIds`, `pinterestConnectionIds`, `youtubeConnectionIds`. Facebook uses `pageIds`. - TikTok configs **require** `privacyLevel` — always set it (e.g., `PUBLIC_TO_EVERYONE`). - Pinterest configs **require** `boardId` — there is no way to fetch boards via this API currently, so ask the user which board to use. - For carousels, upload multiple files and include all public URLs in `mediaUrls`. - Use `platformTexts` to customize text per platform when cross-posting. - Content types: `TEXT` (no media), `IMAGE` (single image), `VIDEO` (single video), `CAROUSEL` (multiple images/videos). - Check `skippedPlatforms` in the response — it tells you if any platform was skipped and why.

path: references/platform-configs.md
content: # Platform-Specific Configs Reference

Platform configs are passed as arrays in both `POST /social-posts` and `POST /social-posts/bulk` request bodies. Each config object is tied to a specific connection via `connectionId` (or `pageId` for Facebook).

For bulk scheduling, configs can be set at two levels:
- **Batch-level** (top-level request body) — applied to all posts as the default
- **Per-post** (inside individual post items) — overrides batch-level for that specific post

## TikTok — `tiktokConfigs`

```json
{
  "tiktokConfigs": [
    {
      "connectionId": "tiktok-connection-id",
      "privacyLevel": "PUBLIC_TO_EVERYONE",
      "title": "Video title",
      "caption": "Extended caption text",
      "allowComments": true,
      "allowDuet": true,
      "allowStitch": true,
      "sendAsDraft": false,
      "aiGenerated": false,
      "brandedContent": false,
      "brandedContentOwnBrand": false,
      "autoAddMusic": false
    }
  ]
}
```

| Parameter                | Type    | Required | Description                                                                       |
| ------------------------ | ------- | -------- | --------------------------------------------------------------------------------- |
| `connectionId`           | string  | **yes**  | TikTok account connection ID                                                      |
| `privacyLevel`           | string  | **yes**  | `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY` |
| `title`                  | string  | no       | Video title (max 90 chars)                                                        |
| `caption`                | string  | no       | Extended caption (max 2,200 chars)                                                |
| `allowComments`          | boolean | no       | Allow comments on the video                                                       |
| `allowDuet`              | boolean | no       | Allow duets                                                                       |
| `allowStitch`            | boolean | no       | Allow stitches                                                                    |
| `sendAsDraft`            | boolean | no       | Save as draft in TikTok app                                                       |
| `aiGenerated`            | boolean | no       | Mark content as AI-generated                                                      |
| `brandedContent`         | boolean | no       | Sponsored/partnership content                                                     |
| `brandedContentOwnBrand` | boolean | no       | Self-promotional content                                                          |
| `autoAddMusic`           | boolean | no       | Auto-add background music                                                         |

**Media notes:**

- Video: MP4/MOV, H.264, max 250MB, 3s-10min. Best: 15-30s, 1080x1920 (9:16)
- Carousels: 2-35 images (photo slideshows)
- Caption: max 2,200 characters

## Instagram — `instagramConfigs`

```json
{
  "instagramConfigs": [
    {
      "connectionId": "instagram-connection-id",
      "postType": "REEL"
    }
  ]
}
```

| Parameter      | Type   | Required | Description                                 |
| -------------- | ------ | -------- | ------------------------------------------- |
| `connectionId` | string | **yes**  | Instagram account connection ID             |
| `postType`     | string | no       | `FEED`, `REEL`, or `STORY`. Default: `FEED` |

**Content types:**

- **FEED**: Feed posts. Single image, carousel (up to 10), or video
- **REEL**: Short-form video, 3-90 seconds, 9:16 recommended. Gets 2-3x reach vs feed
- **STORY**: 24-hour temporary content. Image or video

**Media notes:**

- Video: max 1GB for Reels
- Carousels: up to 10 images or videos

## Facebook — `facebookConfigs`

```json
{
  "facebookConfigs": [
    {
      "pageId": "facebook-page-id",
      "postType": "REEL",
      "videoTitle": "My Video Title"
    }
  ]
}
```

| Parameter    | Type   | Required | Description                                  |
| ------------ | ------ | -------- | -------------------------------------------- |
| `pageId`     | string | **yes**  | Facebook page ID (from social accounts list) |
| `postType`   | string | no       | `FEED`, `REEL`, or `STORY`. Default: `FEED`  |
| `videoTitle` | string | no       | Video title (max 255 chars)                  |

**Content types:**

- **FEED**: Permanent feed content. Up to 10 images OR 1 video. Text limit: 63,206 chars
- **REEL**: Short-form vertical video
- **STORY**: 24-hour temporary content

**Media notes:**

- Images: JPG/PNG, max 30MB each, up to 10 per post
- Cannot mix images and videos in same post

## YouTube — `youtubeConfigs`

```json
{
  "youtubeConfigs": [
    {
      "connectionId": "youtube-connection-id",
      "postType": "SHORTS",
      "videoTitle": "My Video Title",
      "tags": ["tutorial", "howto"],
      "privacyStatus": "public",
      "license": "youtube",
      "notifySubscribers": true,
      "allowEmbedding": true,
      "madeForKids": false,
      "categoryId": "22",
      "playlistId": "PLxxxxxxxx"
    }
  ]
}
```

| Parameter           | Type     | Required | Description                                           |
| ------------------- | -------- | -------- | ----------------------------------------------------- |
| `connectionId`      | string   | **yes**  | YouTube channel connection ID                         |
| `postType`          | string   | no       | `VIDEO` or `SHORTS`. Default: `VIDEO`                 |
| `videoTitle`        | string   | no       | Video title (max 100 chars)                           |
| `tags`              | string[] | no       | Video tags (max 20 tags)                              |
| `privacyStatus`     | string   | no       | `public`, `private`, or `unlisted`. Default: `public` |
| `license`           | string   | no       | `youtube` or `creativeCommon`                         |
| `notifySubscribers` | boolean  | no       | Notify subscribers on publish                         |
| `allowEmbedding`    | boolean  | no       | Allow embedding on other sites                        |
| `madeForKids`       | boolean  | no       | COPPA compliance flag                                 |
| `categoryId`        | string   | no       | YouTube category ID                                   |
| `playlistId`        | string   | no       | Add to playlist after publishing                      |

**Media notes:**

- Shorts: up to 3 minutes, 9:16 or 1:1
- Copyrighted music limits Shorts to 60 seconds
- H.264 video codec with AAC audio recommended

## Pinterest — `pinterestConfigs`

```json
{
  "pinterestConfigs": [
    {
      "connectionId": "pinterest-connection-id",
      "boardId": "board-id-here",
      "title": "Pin Title",
      "link": "https://example.com/page"
    }
  ]
}
```

| Parameter      | Type   | Required | Description                                             |
| -------------- | ------ | -------- | ------------------------------------------------------- |
| `connectionId` | string | **yes**  | Pinterest account connection ID                         |
| `boardId`      | string | **yes**  | Target board ID                                         |
| `title`        | string | no       | Pin title (max 100 chars)                               |
| `link`         | string | no       | Destination URL when pin is clicked (must be valid URL) |

**Media notes:**

- Ideal aspect ratio: 2:3 (1000x1500)
- Carousels: 2-5 static images only (no video in carousels)

## Platforms Without Config Objects

These platforms use only connection ID arrays — no additional config:

### X (Twitter)

- Uses `twitterConnectionIds` array
- Character limit: 280
- Up to 4 images per post
- No video support via this API

### Bluesky

- Uses `blueskyConnectionIds` array
- Character limit: 300
- Up to 4 images
- No video support
- URLs auto-generate link cards
- No editing after publishing

### Threads

- Uses `threadsConnectionIds` array
- Text + images + video supported
- Carousels: up to 10 images

### LinkedIn

- Uses `linkedinConnectionIds` array
- Character limit: 3,000 (under 1,300 performs better)
- Up to 9 images, or video up to 10 min
- Put links in comments, not post body (LinkedIn deprioritizes external links)
- 3-5 hashtags max


path: references/api-reference.md
content: # AdaptlyPost API Reference

Base URL: `https://post.adaptlypost.com/post/api/v1`
Auth: `Authorization: Bearer <api-token>` header. Tokens start with `adaptly_` prefix.

## Endpoints

### GET /social-accounts

List all connected social media accounts for the account group tied to this API token.

**Response:**

```json
{
  "accounts": [
    {
      "id": "cmlxmnxn20006hzpzvo291ckg",
      "platform": "INSTAGRAM",
      "displayName": "John Doe",
      "username": "johndoe",
      "avatarUrl": "https://..."
    }
  ]
}
```

Platform values: `TIKTOK`, `INSTAGRAM`, `FACEBOOK`, `TWITTER`, `YOUTUBE`, `LINKEDIN`, `THREADS`, `BLUESKY`, `PINTEREST`

**Notes:**

- Facebook accounts represent pages, not personal profiles
- LinkedIn and YouTube accounts may have empty `username`
- Bluesky `username` is the handle (e.g., `user.bsky.social`)

### POST /social-posts

Create or schedule a post to one or more social media platforms.

**Request:**

```json
{
  "platforms": ["TWITTER", "INSTAGRAM"],
  "contentType": "IMAGE",
  "text": "Post text with #hashtags",
  "platformTexts": [{ "platform": "TWITTER", "text": "Short version for X" }],
  "mediaUrls": ["https://cdn.adaptlypost.com/social-media-posts/uuid/photo.jpg"],
  "thumbnailUrl": "https://cdn.adaptlypost.com/social-media-posts/uuid/thumb.jpg",
  "scheduledAt": "2026-06-15T10:00:00.000Z",
  "timezone": "America/New_York",
  "saveAsDraft": false,
  "twitterConnectionIds": ["connection-id-1"],
  "instagramConnectionIds": ["connection-id-2"],
  "instagramConfigs": [
    {
      "connectionId": "connection-id-2",
      "postType": "FEED"
    }
  ]
}
```

**Required fields:**

- `platforms` (string[]): At least one platform. Values: `FACEBOOK`, `INSTAGRAM`, `THREADS`, `TIKTOK`, `TWITTER`, `BLUESKY`, `LINKEDIN`, `PINTEREST`, `YOUTUBE`
- `contentType` (string): `TEXT`, `IMAGE`, `VIDEO`, or `CAROUSEL`
- `timezone` (string): IANA timezone string (e.g., `America/New_York`, `Europe/London`)

**Optional fields:**

- `text` (string): Default post text for all platforms
- `platformTexts` (array): Per-platform text overrides. Each: `{ "platform": "TWITTER", "text": "..." }`
- `mediaUrls` (string[]): Public URLs of uploaded media files
- `thumbnailUrl` (string): Thumbnail URL for video posts
- `scheduledAt` (string): ISO 8601 UTC datetime, must be in the future
- `saveAsDraft` (boolean): Save as draft instead of scheduling/publishing
- `pageIds` (string[]): Facebook page IDs
- `tiktokConnectionIds` (string[]): TikTok account connection IDs
- `threadsConnectionIds` (string[]): Threads account connection IDs
- `instagramConnectionIds` (string[]): Instagram account connection IDs
- `twitterConnectionIds` (string[]): X/Twitter account connection IDs
- `blueskyConnectionIds` (string[]): Bluesky account connection IDs
- `linkedinConnectionIds` (string[]): LinkedIn account connection IDs
- `pinterestConnectionIds` (string[]): Pinterest account connection IDs
- `youtubeConnectionIds` (string[]): YouTube account connection IDs
- `pinterestConfigs` (array): Pinterest-specific settings per connection
- `tiktokConfigs` (array): TikTok-specific settings per connection
- `instagramConfigs` (array): Instagram-specific settings per connection
- `facebookConfigs` (array): Facebook-specific settings per page
- `youtubeConfigs` (array): YouTube-specific settings per connection

See [platform-configs.md](platform-configs.md) for detailed config schemas.

**Response:**

```json
{
  "postId": "cmm0z0k3q0000i0r5mxn0hfhs",
  "queuedPlatforms": ["TWITTER", "INSTAGRAM"],
  "skippedPlatforms": [
    {
      "platform": "FACEBOOK",
      "reason": "No valid connection found"
    }
  ],
  "isScheduled": true,
  "scheduledAt": "2026-06-15T10:00:00.000Z"
}
```

### GET /social-posts

List posts for the authenticated account group with pagination.

**Query parameters:**

- `limit` (integer, optional): Number of posts to return. Range: 1-100. Default: 20
- `offset` (integer, optional): Number of posts to skip. Min: 0. Default: 0

**Example:**

```
GET /social-posts?limit=10&offset=0
```

**Response:**

```json
{
  "posts": [
    {
      "id": "cmm0z0k3q0000i0r5mxn0hfhs",
      "createdAt": "2026-02-24T19:00:34.114Z",
      "updatedAt": "2026-02-24T19:00:34.114Z",
      "userId": "user_01KH48SNHMPJNFYPJWZVJAKXDS",
      "contentType": "TEXT",
      "text": "Hello from API!",
      "scheduledAt": "2026-06-15T10:00:00.000Z",
      "timezone": "America/New_York",
      "status": "DRAFT",
      "platforms": [
        {
          "id": "cmm0z0k3u0001i0r5dlbfa440",
          "createdAt": "2026-02-24T19:00:34.114Z",
          "updatedAt": "2026-02-24T19:00:34.114Z",
          "platform": "TWITTER",
          "status": "PENDING",
          "connectionId": "cmlxly42t0004hzq1bh9kqpwl",
          "mediaUrls": [],
          "youtubeTags": []
        }
      ]
    }
  ],
  "total": 1,
  "hasMore": false
}
```

**Post status values:** `PENDING`, `PROCESSING`, `PUBLISHED`, `FAILED`, `DRAFT`
**Platform status values:** `PENDING`, `PROCESSING`, `PUBLISHED`, `FAILED`, `RETRY`

### GET /social-posts/:id

Get a single post by ID. Returns 404 if not found or not in the account group.

**Response:**
Same structure as individual post in the list endpoint.

**Error response (404):**

```json
{
  "message": "Post not found or access denied",
  "error": "Not Found",
  "statusCode": 404
}
```

### POST /social-posts/bulk

Schedule up to 100 posts at once. Each post can have its own content, media, and scheduled time.

**Request:**

```json
{
  "platforms": ["YOUTUBE", "PINTEREST"],
  "timezone": "America/New_York",
  "youtubeConnectionIds": ["conn_yt123"],
  "pinterestConnectionIds": ["conn_pin456"],
  "youtubeConfigs": [{ "connectionId": "conn_yt123", "postType": "SHORTS", "privacyStatus": "public" }],
  "pinterestConfigs": [{ "connectionId": "conn_pin456", "boardId": "board_abc", "title": "Default title" }],
  "posts": [
    {
      "contentType": "VIDEO",
      "text": "First video",
      "mediaUrls": ["https://cdn.adaptlypost.com/uploads/video1.mp4"],
      "scheduledAt": "2026-03-15T10:00:00Z"
    },
    {
      "contentType": "VIDEO",
      "text": "Second video with custom YouTube config",
      "mediaUrls": ["https://cdn.adaptlypost.com/uploads/video2.mp4"],
      "scheduledAt": "2026-03-15T14:00:00Z",
      "youtubeConfigs": [{ "connectionId": "conn_yt123", "postType": "VIDEO", "videoTitle": "Full tutorial", "privacyStatus": "unlisted" }]
    }
  ]
}
```

**Required fields:**

- `platforms` (string[]): At least one platform
- `timezone` (string): IANA timezone string
- `posts` (array): 1-100 post items

**Optional fields (batch-level):**

- Connection ID arrays: `twitterConnectionIds`, `linkedinConnectionIds`, `instagramConnectionIds`, `tiktokConnectionIds`, `youtubeConnectionIds`, `pinterestConnectionIds`, `blueskyConnectionIds`, `threadsConnectionIds`, `pageIds`
- Platform configs (applied to all posts as default): `pinterestConfigs`, `tiktokConfigs`, `instagramConfigs`, `facebookConfigs`, `youtubeConfigs`

See [platform-configs.md](platform-configs.md) for config schemas.

**Post item fields:**

- `contentType` (string, required): `TEXT`, `IMAGE`, `VIDEO`, or `CAROUSEL`
- `scheduledAt` (string, required): ISO 8601 UTC datetime
- `text` (string): Post text
- `platformTexts` (array): Per-platform text overrides
- `mediaUrls` (string[]): Media file URLs
- `thumbnailUrl` (string): Thumbnail URL for video posts
- `thumbnailTimestampMs` (number): Thumbnail position in video (ms)
- Platform config overrides (per-post): `pinterestConfigs`, `tiktokConfigs`, `instagramConfigs`, `facebookConfigs`, `youtubeConfigs` — when set on a post item, these override the batch-level configs for that specific post

**Response:**

```json
{
  "totalScheduled": 2,
  "totalFailed": 0,
  "results": [
    { "postId": "post_abc001", "success": true, "isScheduled": true, "scheduledAt": "2026-03-15T10:00:00Z", "errorMessage": null },
    { "postId": "post_abc002", "success": true, "isScheduled": true, "scheduledAt": "2026-03-15T14:00:00Z", "errorMessage": null }
  ]
}
```

**Notes:**

- Maximum 100 posts per request
- Each post is processed independently — if one fails, others still schedule
- Only one account per platform allowed (platform ToS compliance)
- Per-post platform configs completely replace batch-level configs (no merging)

### POST /upload-urls

Get presigned upload URLs for media files. Upload 1-20 files per request.

**Request:**

```json
{
  "files": [
    { "fileName": "photo.jpg", "mimeType": "image/jpeg" },
    { "fileName": "video.mp4", "mimeType": "video/mp4" }
  ]
}
```

**Supported MIME types:**

- `image/jpeg` — JPEG images
- `image/png` — PNG images
- `image/webp` — WebP images
- `video/mp4` — MP4 video
- `video/quicktime` — MOV video

**Response:**

```json
{
  "urls": [
    {
      "fileName": "photo.jpg",
      "uploadUrl": "https://...presigned-s3-url...",
      "publicUrl": "https://cdn.adaptlypost.com/social-media-posts/uuid/photo.jpg",
      "key": "social-media-posts/uuid/photo.jpg",
      "expiresAt": "2026-02-24T20:00:00.000Z"
    }
  ]
}
```

**Upload flow:**

1. Call this endpoint to get `uploadUrl` and `publicUrl`
2. PUT the raw file binary to `uploadUrl` with matching `Content-Type` header
3. Use the `publicUrl` in `mediaUrls` when creating a post

## Enums

**PlatformType:**
`FACEBOOK`, `INSTAGRAM`, `THREADS`, `TIKTOK`, `TWITTER`, `BLUESKY`, `LINKEDIN`, `PINTEREST`, `YOUTUBE`

**ContentType:**
`TEXT`, `IMAGE`, `VIDEO`, `CAROUSEL`

**TikTokPrivacyLevel:**
`PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY`

**MetaVideoPostType (Instagram & Facebook):**
`FEED`, `REEL`, `STORY`

**YouTubePostType:**
`VIDEO`, `SHORTS`

**YouTubePrivacyStatus:**
`public`, `private`, `unlisted`

**YouTubeLicense:**
`youtube`, `creativeCommon`

## Error Responses

- `400` — Bad request (missing fields, invalid data, validation errors)
- `401` — Invalid, expired, or missing API token
- `404` — Resource not found or access denied

**Example error:**

```json
{
  "message": ["timezone must be a string", "timezone should not be empty"],
  "error": "Bad Request",
  "statusCode": 400
}
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)