# Upload-Post API Documentation

> Upload-Post is a unified social media posting API: publish videos, photos, text and documents to TikTok, Instagram, YouTube, LinkedIn, Facebook, X (Twitter), Threads, Pinterest, Reddit, Bluesky, Discord, Telegram and Google Business Profile with a single REST call (POST https://api.upload-post.com/api/upload). Includes scheduling, analytics, comments/DMs, webhooks, an FFmpeg API and white-label integration.

This file contains the full documentation of the Upload-Post API in markdown, one page per section. Each section starts with the page title and its canonical URL. Raw markdown for any single page is also available at <page URL>.md (e.g. https://docs.upload-post.com/api/upload-video.md).


---
# AutoDM Monitors
URL: https://docs.upload-post.com/api/autodms

# AutoDM Monitors

Set up persistent monitors that automatically send private DMs to users who comment on your Instagram posts. Monitors run in the background 24/7 — no need to keep polling manually.

---

## Start a Monitor

Create a new AutoDM monitor for an Instagram post. The monitor will check for new comments at regular intervals and send a private reply (DM) to each new commenter.

### Endpoint

```
POST /api/uploadposts/autodms/start
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name                | Type           | Required | Description                                                                                     |
|---------------------|----------------|----------|-------------------------------------------------------------------------------------------------|
| post_url            | String         | Yes      | The Instagram post URL to monitor for comments.                                                |
| reply_message       | String         | Yes      | The DM message to send to each matching commenter.                                             |
| profile_username    | String         | Yes      | Profile username (as configured in Upload-Post). Must have Instagram connected.                |
| buttons             | Array          | No       | Up to 3 `web_url` buttons added to each auto-DM. Each item is an object `{title, url}` (title max 20 chars, url must be http/https). |
| monitoring_interval | Integer        | No       | Minutes between comment checks. Default: `15`. Minimum: `15`.                                 |
| trigger_keywords    | Array / String | No       | Keywords to filter comments. Only comments containing at least one keyword will receive a DM. Case-insensitive and accent-insensitive (`"guide"` matches "GUIDE", "guía", "güide", etc.). If omitted, all commenters receive a DM. Accepts a single string or an array of strings. |

### Limits

- **2 new monitors per profile per day.** You can create up to 2 monitors per profile in a 24-hour period.
- **No duplicate posts.** If there's already an active monitor for a post URL, you must stop it before creating a new one.
- **Auto-expiration.** Monitors automatically stop after **15 days**.
- **Daily DM limits per plan.** Free: 10 DMs/day. Paid: 500 DMs/day. When the limit is reached, the monitor pauses and resumes the next day.

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/autodms/start \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "post_url": "https://www.instagram.com/p/ABC123/",
    "reply_message": "Hey! Here is your free guide: https://example.com/guide",
    "profile_username": "my-profile",
    "monitoring_interval": 15,
    "trigger_keywords": ["guide", "link"],
    "buttons": [
      { "title": "Get the guide", "url": "https://example.com/guide" }
    ]
  }'
```

### Responses

- **200 OK** (monitor started)

```json
{
  "success": true,
  "message": "AutoDMs monitoring started successfully",
  "monitor_id": "user@example.com_my-profile_1234567890",
  "config": {
    "post_url": "https://www.instagram.com/p/ABC123/",
    "reply_message": "Hey! Here is your free guide: https://example.com/guide",
    "profile_username": "my-profile",
    "monitoring_interval": 15
  }
}
```

- **400 Bad Request** (missing fields, no Instagram connected, or duplicate post)

```json
{
  "success": false,
  "error": "There is already an active monitor for this post. Stop it first before creating a new one."
}
```

- **429 Too Many Requests** (daily limit reached)

```json
{
  "success": false,
  "error": "Limit reached: maximum 2 monitors per profile per day."
}
```

---

## Get Monitor Status

Retrieve the status of AutoDM monitors for your account. By default returns active monitors (`running` / `paused`). Pass `include_inactive=true` to also receive stopped and expired monitors so you can recover monitor IDs without keeping your own database.

### Endpoint

```
GET /api/uploadposts/autodms/status
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Query Parameters

| Name             | Type   | Required | Description                                                                                                       |
|------------------|--------|----------|-------------------------------------------------------------------------------------------------------------------|
| include_inactive | Bool   | No       | When `true`, also returns stopped and expired monitors. Deleted monitors are always excluded. Default: `false`.   |

### Example Requests

```bash
# Default — active monitors only
curl 'https://api.upload-post.com/api/uploadposts/autodms/status' \
  -H 'Authorization: Apikey your-api-key-here'

# All monitors (active + stopped + expired)
curl 'https://api.upload-post.com/api/uploadposts/autodms/status?include_inactive=true' \
  -H 'Authorization: Apikey your-api-key-here'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "monitors": [
    {
      "monitor_id": "user@example.com_my-profile_1234567890",
      "post_url": "https://www.instagram.com/p/ABC123/",
      "reply_message": "Hey! Here is your free guide: https://example.com/guide",
      "profile_username": "my-profile",
      "monitoring_interval": 15,
      "is_active": true,
      "is_running": true,
      "is_paused": false,
      "status": "running",
      "stats": {
        "total_comments": 47,
        "new_comments": 5,
        "successful_replies": 31,
        "failed_replies": 2
      },
      "created_at": "2026-03-28T10:00:00",
      "last_check": "2026-03-28T14:30:00",
      "paused_at": null,
      "stopped_at": null,
      "stop_reason": null
    },
    {
      "monitor_id": "user@example.com_my-profile_1234567000",
      "post_url": "https://www.instagram.com/p/XYZ987/",
      "reply_message": "Hey! Here is your free guide: https://example.com/guide",
      "profile_username": "my-profile",
      "monitoring_interval": 15,
      "is_active": false,
      "is_running": false,
      "is_paused": false,
      "status": "stopped",
      "stats": {
        "total_comments": 120,
        "new_comments": 0,
        "successful_replies": 88,
        "failed_replies": 3
      },
      "created_at": "2026-03-20T10:00:00",
      "last_check": "2026-03-27T18:00:00",
      "paused_at": null,
      "stopped_at": "2026-03-27T18:05:00",
      "stop_reason": null
    }
  ],
  "total_active": 1,
  "total": 2
}
```

**Status values:** `running`, `paused`, `resuming`, `stopped`, `expired`

- `running` — thread is currently checking comments and replying.
- `paused` — temporarily halted (data preserved, can be resumed).
- `resuming` — monitor was active in DB but no live thread; one was just started.
- `stopped` — manually stopped via `POST /autodms/stop`. Only returned when `include_inactive=true`.
- `expired` — auto-stopped after the 15-day lifetime. Only returned when `include_inactive=true`.

**Response fields:**

- `total_active` — count of monitors with status `running`, `paused`, or `resuming`. Unchanged by `include_inactive` so existing dashboards keep working.
- `total` — count of every monitor in the response.
- `stopped_at` / `stop_reason` — only set for monitors with `is_active: false`.

---

## Get Monitor Logs

Retrieve activity logs for a specific monitor.

### Endpoint

```
GET /api/uploadposts/autodms/logs
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Query Parameters

| Name       | Type   | Required | Description              |
|------------|--------|----------|--------------------------|
| monitor_id | String | Yes      | The monitor ID to query. |

### Example Request

```bash
curl 'https://api.upload-post.com/api/uploadposts/autodms/logs?monitor_id=user@example.com_my-profile_1234567890' \
  -H 'Authorization: Apikey your-api-key-here'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "logs": [
    {
      "type": "start",
      "timestamp": "2026-03-28T10:00:00",
      "message": "Monitor started"
    },
    {
      "type": "success",
      "timestamp": "2026-03-28T10:15:00",
      "message": "31 DMs sent successfully"
    }
  ],
  "monitor_info": {
    "post_url": "https://www.instagram.com/p/ABC123/",
    "profile_username": "my-profile",
    "is_active": true
  }
}
```

---

## Pause a Monitor

Temporarily pause a monitor without losing its configuration. The monitor stops checking for comments but can be resumed later.

### Endpoint

```
POST /api/uploadposts/autodms/pause
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name       | Type   | Required | Description              |
|------------|--------|----------|--------------------------|
| monitor_id | String | Yes      | The monitor ID to pause. |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/autodms/pause \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{"monitor_id": "user@example.com_my-profile_1234567890"}'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "message": "Monitor paused successfully"
}
```

---

## Resume a Monitor

Resume a previously paused monitor. It will continue checking for new comments from where it left off.

### Endpoint

```
POST /api/uploadposts/autodms/resume
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name       | Type   | Required | Description               |
|------------|--------|----------|---------------------------|
| monitor_id | String | Yes      | The monitor ID to resume. |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/autodms/resume \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{"monitor_id": "user@example.com_my-profile_1234567890"}'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "message": "Monitor resumed successfully"
}
```

---

## Stop a Monitor

Deactivate a monitor. The monitor stops running but its data is preserved.

### Endpoint

```
POST /api/uploadposts/autodms/stop
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name       | Type   | Required | Description             |
|------------|--------|----------|-------------------------|
| monitor_id | String | Yes      | The monitor ID to stop. |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/autodms/stop \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{"monitor_id": "user@example.com_my-profile_1234567890"}'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "message": "Monitor stopped successfully"
}
```

---

## Delete a Monitor

Permanently delete a monitor and all its data.

### Endpoint

```
POST /api/uploadposts/autodms/delete
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name       | Type   | Required | Description               |
|------------|--------|----------|---------------------------|
| monitor_id | String | Yes      | The monitor ID to delete. |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/autodms/delete \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{"monitor_id": "user@example.com_my-profile_1234567890"}'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "message": "Monitor deleted successfully"
}
```

- **404 Not Found**

```json
{
  "success": false,
  "error": "Monitor not found or not owned by this account."
}
```

---

## Important Notes

1. **Instagram only.** AutoDM monitors currently support Instagram only, using Meta's official [Private Replies API](https://developers.facebook.com/docs/instagram-platform/private-replies/).

2. **One DM per comment.** Each comment can only receive one private reply. Duplicate attempts are automatically prevented.

3. **7-day comment window.** Private replies can only be sent to comments less than 7 days old.

4. **Daily DM limits.** Upload-Post enforces daily DM limits per account (varies by plan). When the limit is reached, the monitor pauses and resumes the next day.

5. **Auto-expiration.** Monitors automatically stop after 15 days to prevent stale monitors from running indefinitely.

6. **Rate limits.** Meta enforces a limit of 200 DMs per hour per Instagram account. The monitor includes built-in delays between DMs to stay within limits.

### How It Works

1. You start a monitor with a post URL and reply message.
2. Every `monitoring_interval` minutes, the monitor checks for new comments.
3. For each new comment, it sends a private DM using Meta's Private Replies API.
4. It tracks which comments have already been replied to, avoiding duplicates.
5. After 15 days, the monitor automatically stops.

### Related Endpoints

- [Instagram Comments](./instagram-comments.md) — Read comments and send one-off private replies manually.
- [Instagram Direct Messages](./instagram-dms.md) — Send follow-up DMs and read conversations.
- [Media List](./instagram-media.md) — Find post IDs and URLs for your Instagram content.


---
# Comments (all platforms)
URL: https://docs.upload-post.com/api/comments

# Comments (all platforms)

List, create, and delete comments on your posts across multiple social networks with a single, consistent API. These endpoints call each platform's native API in real-time using the connection stored for the given profile.

## Platform support

| Platform  | List | Create | Delete | Notes                                                                                                  |
| :-------- | :--: | :----: | :----: | :----------------------------------------------------------------------------------------------------- |
| Instagram |  ✅  |   ✅   |   ✅   | Creating a comment requires `comment_id` (you can only reply to an existing comment).                  |
| Facebook  |  ✅  |   ✅   |   ✅   | Works on comments on your Page posts.                                                                   |
| YouTube   |  ✅  |   ✅   |   ✅   | The connected account **must** be authorized with the `youtube.force-ssl` scope (see below).           |
| LinkedIn  |  ✅  |   ✅   |   ✅   | Organization (company page) posts. Use the post **URN** as `post_id`.                                  |
| TikTok    |  ❌  |   ❌   |   ❌   | **Not supported** — TikTok has no public comments API.                                                 |

> **YouTube scope requirement:** Reading, creating, and deleting comments requires the `youtube.force-ssl` OAuth scope. Accounts connected before this scope was added must **reconnect** their YouTube account in Upload-Post to grant it. Older connections will fail with a permission error until reconnected.

> **LinkedIn post identifier:** For LinkedIn, `post_id` (and `post_url`) is the post **URN**, e.g. `urn:li:ugcPost:1234567890`. This same URN is used to list, create, and delete comments.

> **YouTube post identifier:** For YouTube, `post_id` is the **video ID** (e.g. `dQw4w9WgXcQ`).

---

## List Comments

Retrieve comments on one of your posts. Returns the comments as provided by the target platform.

### Endpoint

```
GET /api/uploadposts/comments
```

### Headers

| Name          | Value                    | Description                     |
| :------------ | :----------------------- | :------------------------------ |
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Query Parameters

| Name       | Type    | Required | Description                                                                                                          |
| :--------- | :------ | :------- | :----------------------------------------------------------------------------------------------------------------- |
| `platform` | String  | No       | One of `instagram`, `facebook`, `youtube`, `linkedin`. Defaults to `instagram`. TikTok is not supported.           |
| `user`     | String  | Yes      | Profile username (as configured in Upload-Post).                                                                    |
| `post_id`  | String  | Yes\*    | Post identifier. YouTube = the video ID; LinkedIn = the post URN (`urn:li:ugcPost:...`); Instagram = numeric media ID. Use `post_id` **or** `post_url`. |
| `post_url` | String  | Yes\*    | Full post URL. Alternative to `post_id`.                                                                            |
| `limit`    | Integer | No       | Maximum comments to return per page.                                                                                |
| `after`    | String  | No       | Pagination cursor returned in the previous response. Pass it back to fetch the next page.                           |

\* Provide either `post_id` or `post_url` (one is required).

### Example Requests

**Instagram:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/comments?platform=instagram&user=my-profile&post_id=17890455123456789&limit=50' \
  -H 'Authorization: Apikey your-api-key-here'
```

**YouTube (post_id is the video ID):**

```bash
curl 'https://api.upload-post.com/api/uploadposts/comments?platform=youtube&user=my-profile&post_id=dQw4w9WgXcQ' \
  -H 'Authorization: Apikey your-api-key-here'
```

**LinkedIn (post_id is the post URN):**

```bash
curl 'https://api.upload-post.com/api/uploadposts/comments?platform=linkedin&user=my-profile&post_id=urn:li:ugcPost:1234567890' \
  -H 'Authorization: Apikey your-api-key-here'
```

### Successful Response (`200 OK`)

```json
{
  "success": true,
  "comments": [
    {
      "id": "17858893269123456",
      "text": "Great post!",
      "timestamp": "2025-06-15T10:30:00+0000",
      "user": {
        "id": "17841400123456789",
        "username": "commenter_user"
      }
    }
  ],
  "pagination": {
    "next_cursor": "QVFIUm9TbGd...",
    "has_next": true
  }
}
```

> The exact comment fields vary by platform (each network returns its own shape). Pagination fields are present when the platform supports cursor-based paging.

### Error Responses

- **400 Bad Request** — missing parameters, invalid post identifier, or an unsupported platform (e.g. `tiktok`).
- **403 Forbidden** — the connected account lacks the required scope (e.g. YouTube not connected with `youtube.force-ssl`).
- **500 Internal Server Error**

---

## Create Comment

Post a comment or a reply. Provide exactly one of `comment_id`, `post_id`, or `post_url` to identify the target:

- `comment_id` → reply to that comment.
- `post_id` / `post_url` → top-level comment on that post.

> **Instagram:** only replies are supported — you **must** provide `comment_id`. Instagram does not allow creating a top-level comment via the API.

### Endpoint

```
POST /api/uploadposts/comments/create
```

### Headers

| Name          | Value                    | Description                     |
| :------------ | :----------------------- | :------------------------------ |
| Authorization | Apikey your-api-key-here | Your API key for authentication |
| Content-Type  | application/json         | Request body format             |

### Body Parameters (JSON)

| Name         | Type   | Required | Description                                                                                                          |
| :----------- | :----- | :------- | :----------------------------------------------------------------------------------------------------------------- |
| `platform`   | String | Yes      | One of `instagram`, `facebook`, `youtube`, `linkedin`. TikTok is not supported.                                    |
| `user`       | String | Yes      | Profile username (as configured in Upload-Post).                                                                    |
| `message`    | String | Yes      | The comment text.                                                                                                   |
| `comment_id` | String | Yes\*    | Reply to this comment. **Required for Instagram.**                                                                  |
| `post_id`    | String | Yes\*    | Top-level comment on this post. LinkedIn = the post URN (`urn:li:ugcPost:...`); YouTube = the video ID.            |
| `post_url`   | String | Yes\*    | Top-level comment on this post (by URL). Alternative to `post_id`.                                                  |

\* Provide exactly **one** of `comment_id`, `post_id`, or `post_url`.

### Example Requests

**Reply to a comment (Instagram):**

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/comments/create \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "instagram",
    "user": "my-profile",
    "comment_id": "17858893269123456",
    "message": "Thanks for your comment!"
  }'
```

**Top-level comment on a LinkedIn post (post_id is the URN):**

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/comments/create \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "linkedin",
    "user": "my-profile",
    "post_id": "urn:li:ugcPost:1234567890",
    "message": "Great update!"
  }'
```

### Successful Response (`200 OK`)

```json
{
  "success": true,
  "id": "17858893269654321",
  "message": "Comment created successfully"
}
```

### Error Responses

- **400 Bad Request** — missing fields, more than one target provided, Instagram without `comment_id`, or an unsupported platform.
- **403 Forbidden** — the connected account lacks the required scope.
- **500 Internal Server Error**

---

## Delete Comment

Delete a comment you own (or that is on your post). Accepts `DELETE` or `POST`.

### Endpoint

```
DELETE /api/uploadposts/comments/delete
```

> This endpoint also accepts `POST` with the same body, for clients that cannot send a body with `DELETE`.

### Headers

| Name          | Value                    | Description                     |
| :------------ | :----------------------- | :------------------------------ |
| Authorization | Apikey your-api-key-here | Your API key for authentication |
| Content-Type  | application/json         | Request body format             |

### Body Parameters (JSON)

| Name         | Type   | Required | Description                                                                                       |
| :----------- | :----- | :------- | :----------------------------------------------------------------------------------------------- |
| `platform`   | String | Yes      | One of `instagram`, `facebook`, `youtube`, `linkedin`. TikTok is not supported.                  |
| `user`       | String | Yes      | Profile username (as configured in Upload-Post).                                                 |
| `comment_id` | String | Yes      | The ID of the comment to delete.                                                                 |
| `post_id`    | String | Yes\*    | **Required for LinkedIn only** — the post URN (`urn:li:ugcPost:...`) the comment belongs to.     |

\* `post_id` is required for LinkedIn; ignored for the other platforms.

### Example Requests

**Delete a Facebook comment:**

```bash
curl -X DELETE https://api.upload-post.com/api/uploadposts/comments/delete \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "facebook",
    "user": "my-profile",
    "comment_id": "17858893269123456"
  }'
```

**Delete a LinkedIn comment (post_id required):**

```bash
curl -X DELETE https://api.upload-post.com/api/uploadposts/comments/delete \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "linkedin",
    "user": "my-profile",
    "comment_id": "urn:li:comment:(urn:li:ugcPost:1234567890,9876543210)",
    "post_id": "urn:li:ugcPost:1234567890"
  }'
```

### Successful Response (`200 OK`)

```json
{
  "success": true,
  "message": "Comment deleted successfully"
}
```

### Error Responses

- **400 Bad Request** — missing fields, LinkedIn without `post_id`, or an unsupported platform.
- **403 Forbidden** — the connected account lacks the required scope, or you do not own the comment.
- **500 Internal Server Error**

---

## Related

- [Instagram private & public replies](./instagram-comments.md) — Instagram-specific private-reply DMs and public replies.
- [Direct Messages](./instagram-dms.md) — send DMs to a commenter using their user ID.


---
# Connect API (Build Your Own Connect Page)
URL: https://docs.upload-post.com/api/connect-api

# Connect API — Build Your Own Connect Page

The Connect API lets you build the "connect your social accounts" experience **inside your own product, on your own domain, with your own design** — instead of sending end users to the hosted `access_url` page described in the [White-label Integration Guide](../guides/user-profile-integration.md).

Your page requests a platform authorize URL from Upload-Post, redirects the end user to the social network's consent screen, and Upload-Post handles the OAuth exchange and token storage. After the connection completes, the user is sent back to the URL you choose.

**When to use which:**

| | Hosted page (`access_url`) | Connect API (this page) |
|---|---|---|
| Setup effort | None — one API call | You build the UI |
| Branding | Logo, title, colors, language | 100% yours — it's your page |
| Domain the user sees | `app.upload-post.com` | Yours (except the OAuth hops) |

> **Note:** During the OAuth flow the user always visits the social network's own consent screen, and the network redirects briefly through `app.upload-post.com` (the OAuth callback registered with each platform) before returning to your `redirect_url`. This hop lasts milliseconds and is required by the platforms' OAuth policies — it applies to every provider in the industry.

## How it works

```
Your backend                Your connect page             Upload-Post              Social network
     │                            │                            │                        │
     │ 1. generate-jwt (API key)  │                            │                        │
     │───────────────────────────>│                            │                        │
     │    profile JWT             │                            │                        │
     │                            │ 2. POST /oauth/<platform>/start (profile JWT)      │
     │                            │───────────────────────────>│                        │
     │                            │    authorize_url + state   │                        │
     │                            │ 3. redirect user ──────────────────────────────────>│
     │                            │                            │  4. user authorizes    │
     │                            │                            │<── code + state ───────│
     │                            │                            │  5. token exchange,    │
     │                            │                            │     account stored     │
     │                            │<── 6. redirect to your redirect_url ────────────────│
```

1. Your **backend** calls [`generate-jwt`](./user-profiles.md) with your API key to obtain a profile token for the end user's profile.
2. Your **connect page** calls the start endpoint below with that profile token and receives the `authorize_url`.
3. You redirect the user to `authorize_url` (the social network's consent screen).
4. After consent, the network redirects to Upload-Post's registered callback, which completes the exchange — authenticated by the single-use `state`, so the user's browser needs no session with Upload-Post.
5. The user lands back on your `redirect_url` with `?connect_status=success&platform=<platform>`.

## Start endpoint

```
POST /api/uploadposts/oauth/{platform}/start
```

**Supported platforms:** `tiktok`, `instagram`, `facebook`, `linkedin`, `youtube`, `x` (alias: `twitter`), `threads`, `reddit`, `pinterest`, `google-business`, `snapchat`

### Authentication

Any of:

- **Profile JWT** (recommended for browser calls): `Authorization: Bearer PROFILE_JWT` — the token from [`generate-jwt`](./user-profiles.md). The profile is taken from the token.
- **API key** (server-to-server): `Authorization: Apikey YOUR_API_KEY` — pass the profile in the body.

Never expose your API key in a browser; use the profile JWT there.

### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `profile` | string | Only with API key auth | Profile username the connected account will be linked to. Ignored when a profile JWT is used. |
| `redirect_url` | string | No | Absolute `http(s)` URL (max 2000 chars) to send the end user back to after a successful connection. `?connect_status=success&platform=<platform>` is appended. |

### Response

```json
{
  "success": true,
  "platform": "instagram",
  "authorize_url": "https://www.instagram.com/oauth/authorize?client_id=...&state=...",
  "state": "e34zUEyZjWy6EFeI7IzGSEiUfY8O4K4_",
  "expires_in": 900
}
```

Redirect the end user to `authorize_url` **within 15 minutes** (`expires_in`) — the underlying `state` is single-use and expires after that window. Mint a fresh one per connection attempt.

### Example — browser (profile JWT)

```javascript
const resp = await fetch(
  'https://api.upload-post.com/api/uploadposts/oauth/instagram/start',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${profileJwt}`,
    },
    body: JSON.stringify({
      redirect_url: 'https://yourapp.com/social/connected',
    }),
  }
);
const { authorize_url } = await resp.json();
window.location.href = authorize_url;
```

### Example — server (API key)

```bash
curl -X POST "https://api.upload-post.com/api/uploadposts/oauth/tiktok/start" \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": "end-user-123",
    "redirect_url": "https://yourapp.com/social/connected"
  }'
```

### Errors

| Status | Meaning |
|---|---|
| `400` | Missing `profile` (API key auth) or invalid `redirect_url` |
| `401` | Missing or invalid authentication |
| `404` | Unknown platform, or profile not found for this account (`error_code: PROFILE_NOT_FOUND`) |
| `500` | Platform client not configured on the server |

## Handling the return

On success the end user arrives at your `redirect_url`:

```
https://yourapp.com/social/connected?connect_status=success&platform=instagram
```

To confirm the connection server-side (recommended), call [`GET /api/uploadposts/users`](./user-profiles.md) and check the profile's `social_accounts`.

If the user cancels on the consent screen or the connection fails, they remain on the Upload-Post error screen with a human-readable message; they can simply retry from your page with a freshly minted start.

## Security model

- The `state` in the authorize URL is a 192-bit random value stored server-side, **single-use** and valid for **15 minutes**. It is what authenticates the OAuth callback, so the end user's browser never needs an Upload-Post session.
- The `state` is bound to the platform, profile and account that minted it — it cannot be replayed, reused across platforms, or combined with another account's session.
- OAuth secrets (PKCE verifiers for TikTok and X) never leave the server; only the S256 challenge appears in the authorize URL.
- `redirect_url` is validated to be an absolute `http(s)` URL and is only used after a successful connection.

## Notes per platform

- **`youtube`**: profiles configured with [custom YouTube credentials](./user-profiles.md) authorize against their own Google client automatically.
- **`x` / `twitter`**: both names are accepted; responses always report `platform: "x"`.
- **`snapchat`**: connection is limited to basic profile scopes (posting requires Snapchat Public Profile API approval).
- **`instagram`**: the end user's Instagram must be a Business or Creator account, same as the hosted flow.


---
# Current User API
URL: https://docs.upload-post.com/api/current-user

# Current User API

Verify the validity of your API key and retrieve basic account information.

## Authentication

```
Authorization: Apikey YOUR_API_KEY
```

---

## Get Current User

Validates your API key and returns the associated email and subscription plan.

### Endpoint

```
GET /api/uploadposts/me
```

### Headers

| Name          | Required | Description                |
|---------------|----------|----------------------------|
| Authorization | Yes      | `Apikey YOUR_API_KEY`      |

### Example Request (curl)

```bash
curl -X GET https://api.upload-post.com/api/uploadposts/me \
  -H "Authorization: Apikey YOUR_API_KEY"
```

### Success Response (200 OK)

```json
{
  "success": true,
  "message": "Token is valid",
  "email": "user@example.com",
  "plan": "Professional",
  "preferences": {
    "weekStartDay": 1
  }
}
```

**Response Fields:**

| Field   | Type    | Description                                                        |
|---------|---------|--------------------------------------------------------------------|
| success | Boolean | Always `true` for successful requests                              |
| message | String  | Confirmation message                                               |
| email   | String  | The email address associated with the authenticated account        |
| plan    | String  | Current subscription plan (e.g., `Basic`, `Professional`, `Business`, `Default`) |
| preferences | Object | User preferences. See [Preferences](#preferences) below.      |

### Error Responses

**401 Unauthorized** - Invalid or missing authentication

```json
{
  "success": false,
  "message": "Invalid or expired token"
}
```

**500 Internal Server Error** - Server-side error

```json
{
  "success": false,
  "message": "Error description"
}
```

---

## Preferences

Manage user-level preferences via the preferences endpoint.

### Get Preferences

```
GET /api/uploadposts/users/preferences
```

**Response (200 OK):**

```json
{
  "success": true,
  "preferences": {
    "weekStartDay": 1
  }
}
```

### Update Preferences

```
POST /api/uploadposts/users/preferences
```

**Body (JSON):**

| Field        | Type    | Description                                     |
|--------------|---------|-------------------------------------------------|
| weekStartDay | Integer | Calendar week start day. `0` = Sunday, `1` = Monday. |

**Example:**

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/preferences \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"weekStartDay": 1}'
```

**Response (200 OK):**

```json
{
  "success": true,
  "preferences": {
    "weekStartDay": 1
  }
}
```

**Error (400 Bad Request):**

```json
{
  "success": false,
  "message": "weekStartDay must be 0 (Sunday) or 1 (Monday)"
}
```

---

## Use Cases

- **Token Validation**: Verify that your API key or JWT is still valid before making other API calls
- **Plan Check**: Determine the current subscription plan to understand available features and limits
- **Account Verification**: Confirm which account is associated with your credentials


---
# FFmpeg Editor API
URL: https://docs.upload-post.com/api/ffmpeg-editor

# FFmpeg Editor API

Process and transform media using your own FFmpeg command safely on our infrastructure. Submit a job with your media and a command template, then poll the job until it finishes and download the result.

### Endpoint

```
POST /api/uploadposts/ffmpeg/jobs/upload
```

### Headers

| Name          | Value                    | Description                      |
|---------------|--------------------------|----------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication. |

### Parameters

| Name              | Type          | Required | Description |
|-------------------|---------------|----------|-------------|
| file              | File (binary) | Yes      | Media file to process. |
| full_command      | String        | Yes      | FFmpeg command template that MUST use `{input}` and `{output}` placeholders. Example: `ffmpeg -y -i {input} -c:v libx264 -crf 23 {output}` |
| output_extension  | String        | Yes      | Desired output file extension (e.g., `mp4`, `wav`, `mp3`, `mov`, `webm`). |

> Note: If the duration of the input media cannot be detected, the system assumes 60 seconds for quota calculation.

### Command Template Rules

For security and reliability, only safe FFmpeg commands are accepted.

 - Use placeholders: `{input}` (or indexed `{input0}`, `{input1}`, …) for input files and `{output}` for the output file; do not hardcode filenames. The first input can be referenced as `{input}` or `{input0}`.
- Allowed pattern starts with `ffmpeg` and may include typical flags (e.g., `-y`, `-i`, `-c:v`, `-c:a`, `-r`, `-b:v`, filters, etc.).
- Blocked characters/constructs to prevent command injection: `;`, `|`, `&`, `$`, `\``, `$(`, and destructive commands like `rm`/`rmdir`.
- Newlines and carriage returns in the command string are automatically replaced with spaces (not blocked), so pasting multi-line commands works. To render multi-line text in the `drawtext` filter, use the literal escape `\n` (backslash + n) — in JSON, send `\\n`.
- If validation fails, the API returns 400 Bad Request with a helpful message.

### Responses

- 202 Accepted (job created)

```json
{
  "success": true,
  "job_id": "a97bbb5a-139b-46ca-b893-6e8d303d5934",
  "status": "PENDING"
}
```

### Check Job Status

Poll the job until it finishes.

```
GET /api/uploadposts/ffmpeg/jobs/{job_id}
```

Example response:

```json
{
  "job_id": "a97bbb5a-139b-46ca-b893-6e8d303d5934",
  "status": "FINISHED",
  "duration_seconds": 120,
  "output_extension": "mp4"
}
```

Statuses: `PENDING`, `PROCESSING`, `FINISHED`, `ERROR`.

### Download Result

When status is `FINISHED`, download the processed file.

```
GET /api/uploadposts/ffmpeg/jobs/{job_id}/download
```

Response headers include the appropriate `Content-Type` and a `Content-Disposition` attachment filename (e.g., `output.mp4`, `output.wav`). The response body is the binary media.

### Example: Convert to MP4 (H.264)

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'file=@/path/to/input.mov' \
  -F 'full_command=ffmpeg -y -i {input} -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 128k {output}' \
  -F 'output_extension=mp4' \
  -X POST https://api.upload-post.com/api/uploadposts/ffmpeg/jobs/upload
```

### Example: Extract Audio to WAV

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'file=@/path/to/input.mp4' \
  -F 'full_command=ffmpeg -y -i {input} -vn -acodec pcm_s16le -ar 44100 -ac 2 {output}' \
  -F 'output_extension=wav' \
  -X POST https://api.upload-post.com/api/uploadposts/ffmpeg/jobs/upload
```

## Concatenate/Merge multiple videos (NEW)

The API now supports multiple input files for operations like concatenation. You can use placeholders `{input0}`, `{input1}`, `{input2}`, etc., in `full_command`.

### Option A: Send multiple URLs (JSON endpoint)

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/ffmpeg/jobs/upload \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Apikey your-api-key-here' \
  -d '{
    "files": [
      "https://tu-cdn.com/video1.mp4",
      "https://tu-cdn.com/video2.mp4",
      "https://tu-cdn.com/video3.mp4"
    ],
    "full_command": "ffmpeg -y -hide_banner -i {input0} -i {input1} -i {input2} -filter_complex \"[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]\" -map \"[outv]\" -map \"[outa]\" -c:v h264_nvenc -preset p5 -cq 23 -c:a aac -b:a 128k {output}",
    "output_extension": "mp4",
    "publish": true
  }'
```

### Option B: Upload multiple files (multipart/form-data)

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/ffmpeg/jobs/upload \
  -H 'Authorization: Apikey your-api-key-here' \
  -F "file=@/ruta/video1.mp4" \
  -F "file1=@/ruta/video2.mp4" \
  -F "file2=@/ruta/video3.mp4" \
  -F 'full_command=ffmpeg -y -hide_banner -i {input0} -i {input1} -i {input2} -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]" -map "[outv]" -map "[outa]" -c:v h264_nvenc -preset p5 -cq 23 -c:a aac -b:a 128k {output}' \
  -F "output_extension=mp4" \
  -F "publish=true"
```

### Concatenation examples

**Simple concatenation (2 videos):**

```json
{
  "files": ["https://cdn.com/part1.mp4", "https://cdn.com/part2.mp4"],
  "full_command": "ffmpeg -y -i {input0} -i {input1} -filter_complex \"[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]\" -map \"[v]\" -map \"[a]\" -c:v h264_nvenc -cq 23 -c:a aac {output}",
  "output_extension": "mp4"
}
```

**Concatenation with re-encoding (multiple videos):**

```json
{
  "files": [
    "https://cdn.com/intro.mp4",
    "https://cdn.com/contenido.mp4",
    "https://cdn.com/outro.mp4"
  ],
  "full_command": "ffmpeg -y -hwaccel cuda -i {input0} -i {input1} -i {input2} -filter_complex \"[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]\" -map \"[outv]\" -map \"[outa]\" -c:v h264_nvenc -preset p5 -rc vbr -cq 23 -b:v 4M -c:a aac -b:a 128k {output}",
  "output_extension": "mp4"
}
```

**Using the concat demuxer (no re-encoding — faster but requires identical formats):**

First create a text file with the list of videos, upload it as `file` and the videos as `file1`, `file2`, etc.:

```bash
# Create list (concat-list.txt)
echo "file '/work/.../in-src-0'" > concat-list.txt
echo "file '/work/.../in-src-1'" >> concat-list.txt
echo "file '/work/.../in-src-2'" >> concat-list.txt

# Note: For this method you'll need to adjust paths in the command or use the filter_complex method above
```

> Important: The `{input}` placeholder still works as before (points to the first file). For multiple inputs, use `{input0}`, `{input1}`, `{input2}`, etc.

> Limitation: The predefined presets (`h264_social`, `hevc_social`, `copy_mux`) and `ffmpeg_args` only support a single file. For multiple inputs, you must use `full_command`.

### Example: Draw Multi-Line Text on Video

Use the `drawtext` filter with `\n` for line breaks. In JSON, escape as `\\n`:

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'file=@/path/to/input.mp4' \
  -F 'full_command=ffmpeg -y -i {input} -vf "drawtext=text='"'"'Line 1\nLine 2\nLine 3'"'"':fontsize=24:fontcolor=white:x=10:y=10" {output}' \
  -F 'output_extension=mp4' \
  -X POST https://api.upload-post.com/api/uploadposts/ffmpeg/jobs/upload
```

JSON body example:

```json
{
  "full_command": "ffmpeg -y -i {input} -vf \"drawtext=text='Hello World\\nSecond Line':fontsize=28:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2\" {output}",
  "output_extension": "mp4",
  "input_url": "https://example.com/video.mp4"
}
```

### Quotas by Plan (minutes of media/month)

| Plan          | Minutes/Month |
|---------------|----------------|
| free          | 30             |
| basic         | 300            |
| professional  | 1000           |
| advanced      | 3000           |
| business      | 10000          |

Resets on the 1st of each month at 00:00 UTC.

### Check Your FFmpeg Consumption

To check your current FFmpeg usage and remaining quota, use:

```
GET /api/uploadposts/ffmpeg/consumption
```

Example response:

```json
{
  "success": true,
  "consumption": {
    "used_minutes": 45.5,
    "remaining_minutes": 254.5,
    "quota_minutes": 300,
    "usage_percentage": 15.2,
    "total_requests": 12,
    "current_month": "2024-01",
    "plan": "basic"
  },
  "history": [
    {
      "duration_seconds": 180,
      "duration_minutes": 3.0,
      "timestamp": "2024-01-15T10:30:00.000Z",
      "month": "2024-01",
      "type": "video_processing"
    }
  ],
  "quota_info": {
    "plan": "basic",
    "quota_minutes": 300,
    "reset_day": 1,
    "next_reset": "2024-02-01"
  }
}
```

You can also view your FFmpeg usage in the [API Keys page](https://app.upload-post.com/api-keys) or your [Profile page](https://app.upload-post.com/profile).

### Errors

- 400 Bad Request: Invalid or unsafe FFmpeg command, missing parameters.
- 401 Unauthorized: Invalid or expired API key.
- 404 Not Found: Job not found.
- 429 Too Many Requests: Monthly quota exceeded (response includes current usage when applicable).
- 500 Internal Server Error: Processing error.

### Notes

- Jobs are asynchronous; always poll the job status before attempting to download the output.
- Quota checks use detected media duration to ensure fair usage across plans.


---
# Analytics API
URL: https://docs.upload-post.com/api/get-analytics

### **GET /api/analytics/profile_username**

Retrieves analytics data for a specified user profile across one or more social media platforms.

---

**Method:** `GET`

**Endpoint URL:** `https://api.upload-post.com/api/analytics/profile_username`

**Description:**

This endpoint provides key analytics metrics for a given social media profile associated with a user's account. It allows fetching data for multiple platforms in a single request. The system is designed to be extensible, with support for more platforms planned for the future.

**Authentication:**

A valid JSON Web Token (JWT) is required for authentication. The token must be included in the `Authorization` header as a Apikey token.

`Authorization: Apikey <YOUR_JWT_TOKEN>`

**Parameters:**

| Parameter          | Type   | Location      | Required | Description                                                                                             |
| ------------------ | ------ | ------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `profile_username` | string | Path          | Yes      | The unique username of the profile for which you want to retrieve analytics.                            |
| `platforms`        | string | Query         | Yes      | A comma-separated list of platforms to fetch analytics for. E.g., `?platforms=instagram,youtube,threads,pinterest,reddit`. |
| `page_id`          | string | Query         | No       | Required for Facebook analytics. The ID of the Facebook Page.                                           |
| `page_urn`         | string | Query         | No       | **LinkedIn only.** Organization/company page URN or numeric ID to fetch analytics for. LinkedIn analytics are available **only for organization/company pages you administer** — personal profiles are **not** supported, because LinkedIn's API does not expose member-level analytics. If omitted, the first administered organization page is used. |

**Supported Platforms:**

Currently, the following platforms are supported:
*   `instagram`
*   `tiktok`
*   `Linkedin`
*   `Facebook`
*   `X` (Twitter)
*   `youtube`
*   `threads`
*   `pinterest`
*   `reddit`
*   `bluesky`

Support for additional platforms will be added in the future. If you request a platform that is not yet supported, the response will include a message indicating this for that specific platform.

> **Note:** Discord, Telegram, and the credential-based channels (Slack, Mastodon, Nostr, Lemmy, Dev.to, Hashnode, WordPress, Whop, Listmonk) do **not** support analytics. Requesting analytics for any of them returns a graceful per-platform message (e.g. `"Analytics are not supported for Discord."`) rather than an error, so a mixed request for other platforms still succeeds.

Here is an example of how to call the endpoint to get analytics for the `test` profile on Instagram, YouTube, Threads, Pinterest, and Reddit.

```bash
curl 'https://api.upload-post.com/api/analytics/test?platforms=instagram,youtube,threads,pinterest,reddit' \
--header 'Authorization: Apikey XXX...'
```

**Example Successful Response (200 OK):**

The response is a JSON object where each key corresponds to a requested platform. The value is another object containing the specific analytics data for that platform.

```json
{
    "instagram": {
        "followers": 47,
        "reach": 1250,
        "views": 3400,
        "impressions": 3400,
        "profileViews": 89,
        "likes": 120,
        "comments": 15,
        "shares": 8,
        "saves": 22,
        "reach_timeseries": [
            {
                "date": "2025-07-04",
                "value": 42
            },
            {
                "date": "2025-07-05",
                "value": 55
            },
            // ... more date entries
            {
                "date": "2025-08-02",
                "value": 38
            }
        ],
        "follower_demographics": {
            "age": { "25-34": 100, "35-44": 166, "45-54": 49 },
            "gender": { "F": 74, "M": 36, "U": 24 },
            "country": { "ES": 78, "CO": 13, "PE": 8 },
            "city": { "Madrid, Comunidad de Madrid": 18, "Barcelona, Cataluña": 2 }
        },
        "engaged_audience_demographics": {
            "age": { "25-34": 61, "35-44": 88, "45-54": 20 },
            "gender": { "F": 44, "M": 21, "U": 9 },
            "country": { "ES": 52, "CO": 7, "PE": 4 },
            "city": { "Madrid, Comunidad de Madrid": 11, "Barcelona, Cataluña": 1 }
        },
        "metric_type": "reach"
    }
}
```

**Field Descriptions for Platform Analytics:**

*   `followers`: Total number of followers.
*   `reach`: The number of unique accounts that have seen any of the profile's content.
*   `views`: Total content views (Instagram, YouTube, TikTok). For Instagram, this is the official "views" metric from the Instagram API, which replaced the deprecated "impressions" metric.
*   `impressions`: Alias for `views` on Instagram, YouTube, and TikTok. On other platforms (X, Pinterest, Threads), this represents content impressions. Kept for backwards compatibility.
*   `profileViews`: Total number of times the profile was viewed. For Instagram, this represents "Accounts Engaged" (unique accounts that interacted with the content).
*   `likes`: Total number of likes across the profile's content.
*   `comments`: Total number of comments across the profile's content.
*   `shares`: Total number of shares across the profile's content.
*   `saves`: Total number of saves across the profile's content.
*   `pin_clicks`: Total number of clicks on pins (Pinterest only).
*   `outbound_clicks`: Total number of clicks to external URLs from pins (Pinterest only).
*   `reach_timeseries`: An array of objects showing the daily reach or views value over the last 30 days. The dashboard filters this data client-side based on the selected date range.
*   `follower_demographics` (Instagram only): Audience breakdown of the profile's **followers** as `{ age, gender, country, city }`, each a map of segment → follower count (gender values `F`/`M`/`U`; country as ISO codes). Sourced from the Instagram `follower_demographics` insight over the last 30 days. **Only returned for accounts with 100+ followers** — smaller accounts return `{}` (Meta restriction).
*   `engaged_audience_demographics` (Instagram only): Same shape as `follower_demographics` (`{ age, gender, country, city }`), but broken down by the accounts that **engaged** with the content rather than by followers. Sourced from the Instagram `engaged_audience_demographics` insight over the last 30 days. See the note below on when Meta populates it.
*   `metric_type`: Indicates what the `reach_timeseries` represents for this platform. Values: `"reach"` (Facebook, Instagram, LinkedIn), `"views"` (YouTube, TikTok, Threads), `"impressions"` (X, Pinterest), `"score"` (Reddit). Use this to avoid double-counting when aggregating across platforms.
*   `primary_impressions_field`: The field name used as the primary metric for aggregation on this platform (e.g., `"reach"` for Instagram, `"impressions"` for YouTube).
*   `available_metrics`: Array of available metric keys for this platform.
*   `metric_labels`: Object mapping metric keys to user-friendly labels for this platform.

**Instagram Demographics (`follower_demographics` and `engaged_audience_demographics`):**

Both objects have the same structure and are broken down by `age`, `gender`, `country` and `city`. They answer two different questions:

| Field | Population measured |
| --- | --- |
| `follower_demographics` | Everyone who follows the account |
| `engaged_audience_demographics` | Only the accounts that engaged with the account's content |

:::warning Meta thresholds
These are Meta restrictions, not Upload-Post ones:

*   `follower_demographics` is only populated for accounts with **100+ followers**.
*   `engaged_audience_demographics` is only populated once the **engaged** audience clears its own floor of roughly **100 accounts**, and it lands with a lag of roughly **48 hours**.

Below those thresholds Meta returns nothing, and the API surfaces that as **empty objects** (`{"age": {}, "gender": {}, "country": {}, "city": {}}` — or `{}`) rather than as an error. A brand-new or small account will therefore see empty demographics for a while; treat empty as "not enough data yet", not as a failure.
:::

For a unified total impressions metric across platforms, see the [Total Impressions](#get-apiuploadpoststotal-impressionsprofile_username) section below.

**Error Responses:**

*   `400 Bad Request`: The `platforms` query parameter is missing or invalid.
*   `401 Unauthorized`: The JWT is missing, invalid, or expired.
*   `404 Not Found`: The specified `profile_username` does not exist for the authenticated user.
*   `500 Internal Server Error`: An unexpected error occurred on the server while fetching the data.

---

---
title: 'Total Impressions API'
---

### **GET /api/uploadposts/total-impressions/profile_username**

Returns a unified "total impressions" metric for a profile, aggregated from daily analytics snapshots across all connected platforms.

---

**Method:** `GET`

**Endpoint URL:** `https://api.upload-post.com/api/uploadposts/total-impressions/profile_username`

**Description:**

This endpoint provides a single, deduplicated "total impressions" metric that intelligently combines reach and views data across platforms. Since different platforms report different types of impression metrics (Facebook and Instagram report "reach", YouTube and TikTok report "views"), this endpoint uses the most representative metric for each platform to avoid double-counting.

You can also request custom metrics aggregation by specifying which metrics to aggregate using the `metrics` parameter.

**Authentication:**

A valid JSON Web Token (JWT) is required. Include it in the `Authorization` header:

`Authorization: Apikey <YOUR_JWT_TOKEN>`

**Parameters:**

| Parameter          | Type   | Location | Required | Description                                                                                                  |
| ------------------ | ------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `profile_username` | string | Path     | Yes      | The unique username of the profile.                                                                          |
| `date`             | string | Query    | No       | Single date in `YYYY-MM-DD` format. If provided, returns impressions for that day only.                      |
| `start_date`       | string | Query    | No       | Start of date range in `YYYY-MM-DD` format. Defaults to 30 days ago.                                        |
| `end_date`         | string | Query    | No       | End of date range in `YYYY-MM-DD` format. Defaults to today.                                                 |
| `period`           | string | Query    | No       | Shortcut for date range: `last_day`, `last_week`, `last_month`, `last_3months`, `last_year`. Overrides `start_date`/`end_date`. |
| `platform`         | string | Query    | No       | Comma-separated list of platforms to filter by. E.g., `?platform=youtube,tiktok`.                            |
| `breakdown`        | string | Query    | No       | Set to `true` to include per-platform and per-day breakdown in the response.                                 |
| `metrics`          | string | Query    | No       | Comma-separated list of metrics to aggregate. E.g., `?metrics=likes,comments,shares`. Available: `followers`, `reach`, `views`, `impressions`, `likes`, `comments`, `shares`, `saves`, `profileViews`, `video_count`, `following`, `pin_clicks`, `outbound_clicks`. When provided, returns a `metrics` object instead of `total_impressions`. |

**Metric Selection per Platform (default mode):**

| Platform   | Metric Used        | Reason                                        |
| ---------- | ------------------ | --------------------------------------------- |
| Facebook   | reach              | Reports unique account reach                  |
| Instagram  | reach              | Reports unique account reach. Note: Instagram renamed "impressions" to "views" in their API; both `views` and `impressions` fields are returned. |
| LinkedIn   | reach              | Reports unique impressions                    |
| YouTube    | impressions/views  | Reports video view counts                     |
| TikTok     | impressions/views  | Reports video view counts                     |
| X          | impressions        | Reports tweet impression counts               |
| Threads    | impressions/views  | Reports content view counts                   |
| Pinterest  | impressions        | Reports pin impression counts                 |
| Reddit     | impressions/score  | Reports post score as impressions proxy       |

**Example Request:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/total-impressions/myprofile?start_date=2026-01-01&end_date=2026-01-31&breakdown=true' \
--header 'Authorization: Apikey XXX...'
```

**Example Response (200 OK):**

```json
{
    "success": true,
    "profile_username": "myprofile",
    "start_date": "2026-01-01",
    "end_date": "2026-01-31",
    "total_impressions": 45230,
    "per_platform": {
        "instagram": 12500,
        "youtube": 18730,
        "tiktok": 8000,
        "facebook": 6000
    },
    "per_day": {
        "2026-01-01": 1200,
        "2026-01-02": 1450,
        "2026-01-03": 980
    }
}
```

**Example with Period Shortcut:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/total-impressions/myprofile?period=last_week' \
--header 'Authorization: Apikey XXX...'
```

```json
{
    "success": true,
    "profile_username": "myprofile",
    "start_date": "2026-02-13",
    "end_date": "2026-02-20",
    "total_impressions": 8523
}
```

**Example with Custom Metrics:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/total-impressions/myprofile?period=last_month&metrics=likes,comments,shares&breakdown=true' \
--header 'Authorization: Apikey XXX...'
```

```json
{
    "success": true,
    "profile_username": "myprofile",
    "start_date": "2026-01-21",
    "end_date": "2026-02-20",
    "metrics": {
        "likes": 4520,
        "comments": 312,
        "shares": 189
    },
    "per_platform": {
        "likes": { "instagram": 2100, "youtube": 1200, "tiktok": 1220 },
        "comments": { "instagram": 150, "youtube": 80, "tiktok": 82 },
        "shares": { "instagram": 90, "youtube": 45, "tiktok": 54 }
    },
    "per_day": {
        "likes": { "2026-01-21": 150, "2026-01-22": 160 },
        "comments": { "2026-01-21": 10, "2026-01-22": 12 },
        "shares": { "2026-01-21": 6, "2026-01-22": 8 }
    }
}
```

**Example with Single Date:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/total-impressions/myprofile?date=2026-02-15' \
--header 'Authorization: Apikey XXX...'
```

```json
{
    "success": true,
    "profile_username": "myprofile",
    "start_date": "2026-02-15",
    "end_date": "2026-02-15",
    "total_impressions": 1523
}
```

**Example with Platform Filter:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/total-impressions/myprofile?platform=youtube,tiktok&breakdown=true' \
--header 'Authorization: Apikey XXX...'
```

```json
{
    "success": true,
    "profile_username": "myprofile",
    "start_date": "2026-01-20",
    "end_date": "2026-02-19",
    "total_impressions": 26730,
    "platforms_filter": ["youtube", "tiktok"],
    "per_platform": {
        "youtube": 18730,
        "tiktok": 8000
    },
    "per_day": {
        "2026-01-20": 850,
        "2026-01-21": 920
    }
}
```

**Error Responses:**

*   `400 Bad Request`: Invalid date format (must be `YYYY-MM-DD`) or invalid metric name.
*   `401 Unauthorized`: The JWT is missing, invalid, or expired.
*   `500 Internal Server Error`: An unexpected error occurred on the server.

---

### **GET /api/uploadposts/post-analytics/request_id**

Returns analytics for a specific post across all platforms it was published to.

---

**Method:** `GET`

**Endpoint URL:** `https://api.upload-post.com/api/uploadposts/post-analytics/request_id`

**Description:**

This endpoint provides per-post analytics by looking up the upload record and cross-referencing with stored analytics snapshots. It returns the post metadata along with platform-specific metrics at the time of posting and the latest available metrics.

**Authentication:**

A valid JSON Web Token (JWT) is required. Include it in the `Authorization` header:

`Authorization: Apikey <YOUR_JWT_TOKEN>`

**Parameters:**

| Parameter    | Type   | Location | Required | Description                              |
| ------------ | ------ | -------- | -------- | ---------------------------------------- |
| `request_id` | string | Path     | Yes      | The request ID of the upload.            |
| `platform`   | string | Query    | No       | Filter to a single platform (e.g., `?platform=x`). When provided, only metrics for that platform are fetched, which is significantly faster than fetching all platforms. |

**Example Request:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/post-analytics/abc123-def456' \
--header 'Authorization: Apikey XXX...'
```

**Example Request (single platform):**

```bash
curl 'https://api.upload-post.com/api/uploadposts/post-analytics/abc123-def456?platform=youtube' \
--header 'Authorization: Apikey XXX...'
```

**Example Response (200 OK):**

```json
{
    "success": true,
    "post": {
        "request_id": "abc123-def456",
        "profile_username": "myprofile",
        "post_title": "My Video",
        "post_caption": "Check this out!",
        "media_type": "video",
        "upload_timestamp": "2026-02-10 14:30:00"
    },
    "platforms": {
        "youtube": {
            "success": true,
            "platform_post_id": "dQw4w9WgXcQ",
            "post_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
            "post_metrics": {
                "views": 5200,
                "likes": 120,
                "comments": 8,
                "favorites": 3
            },
            "post_metrics_source": "platform_api",
            "profile_snapshot_at_post_date": {
                "followers": 1500,
                "impressions": 45000,
                "likes": 320,
                "comments": 15,
                "shares": 8
            },
            "profile_snapshot_latest": {
                "followers": 1650,
                "impressions": 52000,
                "likes": 410,
                "comments": 22,
                "shares": 12
            },
            "profile_snapshot_latest_date": "2026-02-20"
        },
        "tiktok": {
            "success": true,
            "platform_post_id": "7123456789",
            "post_url": "https://tiktok.com/@user/video/7123456789",
            "post_metrics_error": "TikTok video not found (ID: 7123456789). The token may need to be refreshed.",
            "profile_snapshot_at_post_date": {
                "followers": 800,
                "impressions": 12000,
                "likes": 500,
                "comments": 30
            },
            "profile_snapshot_latest": {
                "followers": 950,
                "impressions": 18500,
                "likes": 780,
                "comments": 45
            },
            "profile_snapshot_latest_date": "2026-02-20"
        }
    }
}
```

**Per-Platform Response Fields:**

*   `success`: Whether the post was successfully published to this platform.
*   `platform_post_id`: The post's ID on the platform.
*   `post_url`: Direct URL to the published post.
*   `post_metrics`: Live metrics fetched from the platform's API for this specific post (views, likes, comments, shares, etc.).
*   `post_metrics_source`: Source of post metrics (currently `"platform_api"`).
*   `post_metrics_error`: If post-level metrics could not be fetched, this field contains a human-readable error message explaining why.
*   `profile_snapshot_at_post_date`: Profile-level metrics snapshot from the day the post was published.
*   `profile_snapshot_latest`: Most recent profile-level metrics snapshot.
*   `profile_snapshot_latest_date`: Date of the latest snapshot.

**Error Responses:**

*   `401 Unauthorized`: The JWT is missing, invalid, or expired.
*   `404 Not Found`: No post found with the given request ID.
*   `500 Internal Server Error`: An unexpected error occurred on the server.

---

### **GET /api/uploadposts/post-analytics?platform\_post\_id=**

Returns analytics for any post (including organically published posts) using its native platform ID instead of a request ID.

---

**Method:** `GET`

**Endpoint URL:** `https://api.upload-post.com/api/uploadposts/post-analytics?platform_post_id=XXXXX&platform=instagram&user=profile_username`

**Description:**

This endpoint allows you to fetch live per-post analytics using the platform's native post ID (e.g., an Instagram media ID) rather than an Upload Post `request_id`. This is useful for retrieving metrics on organically published posts that were not uploaded through the API. You can obtain `platform_post_id` values from the [GET /api/uploadposts/media](./instagram-media.md) endpoint.

:::tip
This endpoint queries the platform live, one post per call, and is limited to **100 requests per 5 minutes**. To read metrics for thousands of posts, use [`GET /api/uploadposts/post-analytics/cached`](#get-apiuploadpostspost-analyticscached) instead, which replays previously fetched results, paginated, and is not subject to that limit.
:::

**Authentication:**

A valid JSON Web Token (JWT) is required. Include it in the `Authorization` header:

`Authorization: Apikey <YOUR_JWT_TOKEN>`

**Parameters:**

| Parameter          | Type   | Location | Required | Description                              |
| ------------------ | ------ | -------- | -------- | ---------------------------------------- |
| `platform_post_id` | string | Query    | Yes      | The native post ID on the platform (e.g., Instagram media ID). |
| `platform`         | string | Query    | Yes      | The platform to query. One of: `youtube`, `tiktok`, `instagram`, `facebook`, `linkedin`, `x`, `threads`, `pinterest`, `reddit`. |
| `user`             | string | Query    | Yes      | The `profile_username` of the profile that owns the social account. |

**Example Request:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/post-analytics?platform_post_id=18330983293218037&platform=instagram&user=myprofile' \
--header 'Authorization: Apikey XXX...'
```

**Example Response (200 OK):**

```json
{
    "success": true,
    "post": {
        "platform_post_id": "18330983293218037",
        "platform": "instagram",
        "profile_username": "myprofile",
        "source": "organic"
    },
    "platforms": {
        "instagram": {
            "success": true,
            "platform_post_id": "18330983293218037",
            "post_metrics": {
                "likes": 340,
                "comments": 12,
                "views": 8500,
                "reach": 6200,
                "impressions": 9100,
                "saves": 45,
                "shares": 28
            },
            "post_metrics_source": "platform_api",
            "available_metrics": ["followers", "reach", "views", "impressions", "profileViews", "likes", "comments", "shares", "saves"],
            "metric_labels": {
                "reach": "Unique Reach",
                "views": "Views",
                "impressions": "Views",
                "profileViews": "Accounts Engaged"
            },
            "primary_impressions_field": "reach"
        }
    }
}
```

**Response Fields:**

*   `post.source`: Either `"organic"` (post was not uploaded via the API) or `"api_uploaded"` (post was uploaded via the API and has an associated `request_id`).
*   `post.request_id`: Only present when `source` is `"api_uploaded"` — the original upload request ID.
*   `platforms.<platform>.post_metrics`: Live metrics fetched from the platform's API.
*   `platforms.<platform>.post_metrics_source`: Source of post metrics (currently `"platform_api"`).
*   `platforms.<platform>.post_metrics_error`: If metrics could not be fetched, a human-readable error message.
*   `platforms.<platform>.available_metrics`: List of metrics available for this platform.

**Error Responses:**

*   `400 Bad Request`: Missing or invalid query parameters.
*   `401 Unauthorized`: The JWT is missing, invalid, or expired.
*   `404 Not Found`: User or profile not found.
*   `500 Internal Server Error`: An unexpected error occurred on the server.

---

### **GET /api/uploadposts/post-analytics/cached**

Reads back, in bulk, per-post metrics that Upload-Post already fetched for you, instead of calling the social platforms again.

---

**Method:** `GET`

**Endpoint URL:** `https://api.upload-post.com/api/uploadposts/post-analytics/cached`

**Description:**

The live per-post endpoints (`/api/uploadposts/post-analytics/{request_id}` and `/api/uploadposts/post-analytics?platform_post_id=`) query the platform APIs on every call, one post at a time, and are therefore subject to the platform analytics rate limit of **100 requests per 5 minutes**. Walking an account with thousands of posts through them is not practical.

This endpoint reads instead from a **write-through cache**: every time a post's metrics are fetched through one of the live endpoints, the result is stored. This endpoint replays those stored values, many posts per call, paginated, and is **not subject to the 100 requests / 5 minutes platform analytics rate limit**, because it never touches the platforms during the request. It is designed for re-reading a large back catalogue you have already fetched once.

:::warning There is no background refresh
This cache is filled as a side effect of live reads. Nothing updates it on its own. Three consequences:

*   A post appears here **only after it has been fetched at least once** through a live per-post endpoint. A post you have never queried live will not be here at all.
*   `captured_at` is **the last time that post was fetched live** — not "this morning". A post you last read a month ago will still show a month-old value.
*   To refresh a post, call the **live** endpoint for it. The cache updates as a side effect of that call.

So the pattern is: fetch live once (or whenever you want fresh numbers), then re-read as many times as you like from here for free. That is where the saving is — customers typically re-read the same post several times a day.
:::

**Authentication:**

A valid JSON Web Token (JWT) is required. Include it in the `Authorization` header:

`Authorization: Apikey <YOUR_JWT_TOKEN>`

**Parameters:**

| Parameter  | Type    | Location | Required | Description                                                                                     |
| ---------- | ------- | -------- | -------- | ------------------------------------------------------------------------------------------------ |
| `user`     | string  | Query    | Yes      | The `profile_username` of the profile to read snapshots for.                                    |
| `platform` | string  | Query    | No       | Restrict the result to a single platform. One of: `instagram`, `tiktok`, `youtube`, `facebook`, `linkedin`, `threads`, `pinterest`, `reddit`. When omitted, all platforms are returned and the response echoes `"platform": null`. |
| `limit`    | integer | Query    | No       | Number of posts per page. Defaults to `50`, maximum `200`.                                       |
| `cursor`   | string  | Query    | No       | Opaque token identifying the next page. Pass back exactly what the previous response returned in `next_cursor`. Do not build or parse these tokens yourself — an invalid cursor returns `400`. |
| `since`    | string  | Query    | No       | Start of the date range in `YYYY-MM-DD` format. Defaults to 30 days ago.                        |
| `until`    | string  | Query    | No       | End of the date range in `YYYY-MM-DD` format. Defaults to today.                                |

**Example Request:**

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl 'https://api.upload-post.com/api/uploadposts/post-analytics/cached?user=influencersde&limit=50' \
--header 'Authorization: Apikey XXX...'
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.get(
    "https://api.upload-post.com/api/uploadposts/post-analytics/cached",
    headers={"Authorization": "Apikey XXX..."},
    params={"user": "influencersde", "limit": 50},
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript
const params = new URLSearchParams({ user: "influencersde", limit: "50" });

const response = await fetch(
  `https://api.upload-post.com/api/uploadposts/post-analytics/cached?${params}`,
  { headers: { Authorization: "Apikey XXX..." } }
);
console.log(await response.json());
```

</TabItem>
</Tabs>

**Example Response (200 OK):**

```json
{
    "success": true,
    "profile_username": "influencersde",
    "platform": null,
    "since": "2026-06-28",
    "until": "2026-07-28",
    "source": "snapshot_cache",
    "posts": [
        {
            "post_id": "uAAYuyTX4P0",
            "platform": "youtube",
            "profile_username": "influencersde",
            "date": "2026-07-28",
            "captured_at": "2026-07-28T11:17:06.901000",
            "metrics": {
                "views": 412,
                "likes": 3,
                "comments": 0,
                "favorites": 0
            },
            "post_url": "https://www.youtube.com/watch?v=uAAYuyTX4P0",
            "media_type": "video",
            "upload_timestamp": "2026-07-17T08:01:50.228000"
        }
    ],
    "limit": 50,
    "next_cursor": "eyJkIjoiMjAyNi0wNy0yOCIsInAiOi...",
    "has_more": true
}
```

**Response Fields:**

*   `profile_username`: The profile the snapshots belong to.
*   `platform`: The platform filter that was applied, or `null` when no `platform` was requested.
*   `since` / `until`: The effective date range covered by the response.
*   `source`: Always `"snapshot_cache"` — a marker that these numbers were replayed from the cache, not fetched from the platform during this request.
*   `posts`: Array of per-post snapshot rows (see below).
*   `limit`: The effective page size that was applied.
*   `next_cursor`: Token to pass as `cursor` on the next request. `null` when there are no more pages.
*   `has_more`: `true` when another page is available.

**Per-Post Fields:**

| Field              | Type        | Description                                                                 |
| ------------------ | ----------- | ----------------------------------------------------------------------------- |
| `post_id`          | String      | The post's native ID on the platform.                                        |
| `platform`         | String      | The platform this row belongs to.                                            |
| `profile_username` | String      | The profile that owns the post.                                              |
| `date`             | String      | Snapshot date (`YYYY-MM-DD`) this row belongs to.                            |
| `captured_at`      | String      | ISO 8601 timestamp of the last time this post was fetched live from the platform. **Use this to judge freshness** — nothing refreshes it in the background. |
| `metrics`          | Object      | The metrics captured for this post. **Keys vary per platform** — see below.  |
| `post_url`         | String/null | Direct URL to the published post.                                            |
| `media_type`       | String/null | Type of media (e.g. `video`, `image`).                                       |
| `upload_timestamp` | String/null | ISO 8601 timestamp of when the post was originally published.                |

:::info Metrics keys vary per platform
`metrics` mirrors what each platform exposes for a post. YouTube returns `views`, `likes`, `comments`, `favorites`; Instagram returns reach/saves/shares-style keys; TikTok, Pinterest and the rest each return their own set. Read the keys present on each row rather than assuming a fixed schema, and treat a missing key as "not reported by that platform".
:::

**Paginating Through Everything:**

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
API_KEY="XXX..."
CURSOR=""

while :; do
  URL="https://api.upload-post.com/api/uploadposts/post-analytics/cached?user=influencersde&limit=200"
  [ -n "$CURSOR" ] && URL="$URL&cursor=$CURSOR"

  RESPONSE=$(curl -s "$URL" -H "Authorization: Apikey $API_KEY")
  echo "$RESPONSE" | jq -r '.posts[] | "\(.platform) \(.post_id)"'

  [ "$(echo "$RESPONSE" | jq -r '.has_more')" = "true" ] || break
  CURSOR=$(echo "$RESPONSE" | jq -r '.next_cursor')
done
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

API_KEY = "XXX..."
all_posts = []
cursor = None

while True:
    params = {"user": "influencersde", "limit": 200, "since": "2026-01-01"}
    if cursor:
        params["cursor"] = cursor

    response = requests.get(
        "https://api.upload-post.com/api/uploadposts/post-analytics/cached",
        headers={"Authorization": f"Apikey {API_KEY}"},
        params=params,
    )
    data = response.json()
    all_posts.extend(data["posts"])

    if not data.get("has_more"):
        break
    cursor = data["next_cursor"]

print(f"Fetched {len(all_posts)} post snapshots")
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript
const API_KEY = "XXX...";
const allPosts = [];
let cursor = null;

while (true) {
  const params = new URLSearchParams({
    user: "influencersde",
    limit: "200",
    since: "2026-01-01",
  });
  if (cursor) params.set("cursor", cursor);

  const response = await fetch(
    `https://api.upload-post.com/api/uploadposts/post-analytics/cached?${params}`,
    { headers: { Authorization: `Apikey ${API_KEY}` } }
  );
  const data = await response.json();
  allPosts.push(...data.posts);

  if (!data.has_more) break;
  cursor = data.next_cursor;
}

console.log(`Fetched ${allPosts.length} post snapshots`);
```

</TabItem>
</Tabs>

**Live vs. Cached:**

| | `GET /api/uploadposts/post-analytics` (live) | `GET /api/uploadposts/post-analytics/cached` |
| --- | --- | --- |
| Data source | Platform API, in real time | Upload-Post's write-through cache |
| Freshness | Current | As of your last live read (`captured_at` tells you) |
| Rate limit | 100 requests / 5 minutes | Not subject to the platform analytics rate limit |
| Scope per call | One post | Up to 200 posts, paginated |
| Covers a post published minutes ago | Yes | Only after you fetch it live once |
| Best for | Refreshing a post's numbers | Re-reading in bulk what you already fetched |

**Error Responses:**

*   `400 Bad Request`: Missing `user`, an invalid `platform`, a malformed `since`/`until` date, or an invalid/expired `cursor`.
*   `401 Unauthorized`: The JWT is missing, invalid, or expired.
*   `404 Not Found`: User or profile not found.
*   `500 Internal Server Error`: An unexpected error occurred on the server.

---

### **GET /api/uploadposts/platform-metrics**

Returns the available metrics configuration for all supported platforms.

---

**Method:** `GET`

**Endpoint URL:** `https://api.upload-post.com/api/uploadposts/platform-metrics`

**Description:**

This public endpoint returns the metrics configuration for each social platform, including which metrics are available and which field is used as the primary "impressions" metric for aggregation.

**Example Response (200 OK):**

```json
{
    "instagram": {
        "primary_impressions_field": "reach",
        "available_metrics": ["followers", "reach", "views", "impressions", "profileViews", "likes", "comments", "shares", "saves"],
        "metric_labels": {
            "reach": "Unique Reach",
            "views": "Views",
            "impressions": "Views",
            "profileViews": "Accounts Engaged"
        }
    },
    "youtube": {
        "primary_impressions_field": "impressions",
        "available_metrics": ["followers", "impressions", "likes", "comments", "shares"],
        "metric_labels": {
            "impressions": "Video Views",
            "followers": "Subscribers"
        }
    }
}
```


---
# Get Facebook Pages
URL: https://docs.upload-post.com/api/get-facebook-pages

This endpoint is crucial for uploads, as it provides you with the necessary `ID` to specify which Facebook Page you want to send your content to.

### **Get Facebook Pages**

This endpoint allows you to get a list of all Facebook pages a user has access to through their connected accounts. This is a necessary step if you want to post to a specific page, as you will need its `ID`.

-   **Method:** `GET`
-   **Endpoint:** `/api/uploadposts/facebook/pages`
-   **Authentication:**
    -   **API Key** in the `Authorization` header.
        -   `Authorization: Apikey <YOUR_API_KEY>`

-   **Query Parameters:**

| Parameter | Type   | Description                                                                                                                                                             | Required |
| :-------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--- |
| `profile` | string | **Optional**. The profile's `username`. If provided, the API will return only the Facebook pages associated with the Facebook account linked to that profile. | No   |

-   **Successful Response (`200 OK`)**

The response will include a list of objects, where each object represents a Facebook page.

```json
{
  "success": true,
  "pages": [
    {
      "id": "109876543210987",
      "name": "My Business Page",
      "picture": "https://url.to/profile/picture.jpg",
      "account_id": "1234567890123456"
    },
    {
      "id": "208765432109876",
      "name": "Travel Blog",
      "picture": "https://url.to/another/picture.jpg",
      "account_id": "1234567890123456"
    }
  ]
}
```

-   **Additional Notes:**
    -   To post on a Facebook page, you must pass the page `id` in the `facebook_page_id` parameter of the upload endpoint (`/api/upload` or `/api/upload_photos`).


---
# Get Google Business Locations
URL: https://docs.upload-post.com/api/get-google-business-locations

This endpoint queries the Google Business Profile API in real-time to return all available locations for a connected account. Use this to let users choose which location to post to.

### **Get Google Business Locations**

-   **Method:** `GET`
-   **Endpoint:** `/api/uploadposts/google-business/locations`
-   **Authentication:**
    -   **API Key** in the `Authorization` header.
        -   `Authorization: Apikey <YOUR_API_KEY>`

-   **Query Parameters:**

| Parameter | Type   | Description                                                                                                                                                             | Required |
| :-------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--- |
| `profile` | string | **Optional**. The profile's `username`. If provided, the API will return only the locations associated with the Google Business account linked to that profile. | No   |

-   **Successful Response (`200 OK`)**

```json
{
  "success": true,
  "locations": [
    {
      "name": "accounts/123456789/locations/111111111",
      "title": "Main Street Store",
      "account_id": "accounts_123456789_111111111"
    },
    {
      "name": "accounts/123456789/locations/222222222",
      "title": "Downtown Branch",
      "account_id": "accounts_123456789_111111111"
    }
  ]
}
```

| Field      | Description                                                    |
| :--------- | :------------------------------------------------------------- |
| `name`     | The Google Business location identifier (used as `gbp_location_id` in uploads) |
| `title`    | Display name of the business location                          |
| `account_id` | The internal Upload-Post account key                        |

---

### **Using Locations in Upload Requests**

The `gbp_location_id` parameter tells the API which location to post to. First list the available locations using the endpoint above, then include the selected location's `name` as `gbp_location_id` in your upload request.

| Parameter        | Type   | Description                                              | Required |
| :--------------- | :----- | :------------------------------------------------------- | :------- |
| `gbp_location_id` | string | The location to post to. Must be a valid location `name` from the locations list. | No*  |

This parameter works with all upload endpoints (`/api/upload`, `/api/upload_photos`, `/api/upload_text`).

> **Auto-select fallback:** If `gbp_location_id` is not provided, the API will automatically query your available locations. If your account has exactly **one** location, it will be used automatically. If you have **multiple** locations, the API returns an error asking you to select one. If you have **zero** locations, the API returns an error indicating no locations were found.

**Example:**

```bash
curl -X POST https://api.upload-post.com/api/upload_text \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -F 'user=my-profile' \
  -F 'platform[]=google_business' \
  -F 'title=Hello from our Downtown Branch!' \
  -F 'gbp_location_id=accounts/123456789/locations/222222222' \
  -F 'gbp_topic_type=STANDARD'
```

---

### **Local Posts vs. the Media/Gallery Tab**

A Google Business upload can target two different places on the location:

| Destination | How to select it | Result |
| :---------- | :--------------- | :----- |
| **Updates tab** (Local Post) | Default. Nothing extra to send. | Creates a Local Post, optionally `STANDARD`, `EVENT` or `OFFER`. |
| **Media / Gallery tab** | `gbp_post_type=MEDIA` (also accepts `PHOTO` or `GALLERY`), or `gbp_upload_to_gallery=true` | Uploads the attached photo straight to the location's photo gallery. |

When targeting the Media/Gallery tab you can also send `gbp_media_category` (alias `media_category`) to file the photo under a category — `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS` or `ADDITIONAL` (the default).

```bash
curl -X POST https://api.upload-post.com/api/upload_photos \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -F 'user=my-profile' \
  -F 'platform[]=google_business' \
  -F 'photos[]=@/path/to/storefront.jpg' \
  -F 'gbp_location_id=accounts/123456789/locations/222222222' \
  -F 'gbp_post_type=MEDIA' \
  -F 'gbp_media_category=EXTERIOR'
```

➡️ Full parameters, response shape and errors: [Publishing to the Media/Gallery tab](./upload-photo.md#publishing-to-the-mediagallery-tab).

-   **Additional Notes:**
    -   Locations are queried live from the Google Business API each time you call this endpoint.
    -   The endpoint handles token refresh automatically if the stored access token has expired.
    -   Connect your Google account via OAuth first — the connection stores your credentials, and this endpoint uses them to fetch locations in real-time.
    -   Works the same way as Facebook pages: connect once, then select which location to post to on each upload.


---
# Get LinkedIn Pages
URL: https://docs.upload-post.com/api/get-linkedin-pages

This endpoint is crucial for uploads, as it provides you with the necessary `ID` to specify which LinkedIn Page you want to send your content to.

### **Get LinkedIn Pages**

Retrieves a list of LinkedIn company pages associated with the authenticated user's account(s).

-   **Method:** `GET`
-   **Endpoint:** `/api/uploadposts/linkedin/pages`
-   **Authentication:**
    -   **Type:** Apikey Token
    -   **Header:** `Authorization: Apikey <YOUR_TOKEN>`

-   **Query Parameters:**

| Parameter | Type   | Description                                                                                                                                                             | Required |
| :-------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--- |
| `profile` | string | **Optional**. The username of a specific profile. If provided, the endpoint will return only the LinkedIn pages associated with the LinkedIn account linked to that profile. If omitted, it will return pages from all LinkedIn accounts connected to the user. | No   |

-   **Successful Response (`200 OK`)**

A JSON object containing a list of the user's LinkedIn pages.

```json
{
  "success": true,
  "pages": [
    {
      "id": "urn:li:organization:12345678",
      "name": "Your Company Name",
      "picture": "https://media.licdn.com/dms/image/C4D0BAQG.../feedshare-logo_300_300.png",
      "account_id": "urn:li:organization:1122334455",
      "vanityName": "your-company-name"
    },
    {
      "id": "urn:li:organization:87654321",
      "name": "Another Company Page",
      "picture": "https://media.licdn.com/dms/image/D4E0BAQ.../feedshare-logo_300_300.png",
      "account_id": "urn:li:organization:1122334455",
      "vanityName": "another-company"
    }
  ]
}
```

-   **Field Descriptions:**
    -   `id`: The unique identifier (URN) for the LinkedIn organization. This is the value you should use when specifying a `target_linkedin_page_id` in other API calls.
    -   `name`: The display name of the LinkedIn page.
    -   `picture`: The URL of the page's logo. Can be `null`.
    -   `account_id`: The internal identifier for the user's connected LinkedIn account in the Upload-Post system.
    -   `vanityName`: The custom "vanity" URL of the page (e.g., the part that comes after `linkedin.com/company/`). Can be `null`.

-   **Error Responses:**
    -   **401 Unauthorized:** If the `Authorization` header is missing or the token is invalid.
    -   **404 Not Found:**
        -   If the user associated with the token is not found.
        -   If a `profile` username is provided but not found for that user.
        -   If no LinkedIn accounts are connected to the user or the specified profile.
        -   If no LinkedIn pages are found for the connected accounts.
    -   **500 Internal Server Error:** If there's an issue communicating with the LinkedIn API or an unexpected server error occurs.


---
# Get Pinterest Boards
URL: https://docs.upload-post.com/api/get-pinterest-boards

This endpoint is crucial for uploads, as it provides you with the necessary `ID` to specify which Pinterest Board you want to send your content to.

### **Get Pinterest Boards**

This endpoint allows you to get a list of all boards (public and secret) from a connected Pinterest account. You will need a board `ID` to post a Pin to it.

-   **Method:** `GET`
-   **Endpoint:** `/api/uploadposts/pinterest/boards`
-   **Authentication:**
    -   **API Key** in the `Authorization` header.
        -   `Authorization: Apikey <YOUR_API_KEY>`

-   **Query Parameters:**

| Parameter | Type   | Description                                                                                                                                                             | Required |
| :-------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--- |
| `profile` | string | **Optional**. The profile's `username`. If provided, the API will return only the boards from the Pinterest account linked to that specific profile.                 | No       |

-   **Successful Response (`200 OK`)**

The response will include a list of objects, where each object represents a Pinterest board.

```json
{
  "success": true,
  "boards": [
    {
      "id": "987654321098765432",
      "name": "Summer Recipes"
    },
    {
      "id": "876543210987654321",
      "name": "Design Inspiration"
    }
  ],
  "pinterest_account_used": "pinterest_username"
}
```

-   **Additional Notes:**
    -   To post a Pin, you must pass the board `id` in the `pinterest_board_id` parameter of the upload endpoint (`/api/upload` or `/api/upload_photos`).
    -   If a `profile` is not specified, the API will use the first Pinterest account it finds connected to the user. The response will tell you which account was used in the `pinterest_account_used` field.


---
# Reddit Detailed Posts
URL: https://docs.upload-post.com/api/get-reddit-detailed-posts

### **GET /api/uploadposts/reddit/detailed-posts/**

Retrieves detailed posts from a Reddit account connected to a profile, including complete media information (images, galleries, and videos).

---

**Method:** `GET`

**Endpoint URL:** `https://api.upload-post.com/api/uploadposts/reddit/detailed-posts/`

**Authentication:**

A valid JSON Web Token (JWT) is required for authentication. The token must be included in the `Authorization` header as a Bearer token.

`Authorization: Bearer <YOUR_JWT_TOKEN>`

**Query Parameters:**

| Parameter          | Type   | Required | Description                                                      |
| ------------------ | ------ | -------- | ---------------------------------------------------------------- |
| `profile_username` | string | Yes      | Username of the profile that has the Reddit account connected.   |

**Example Request:**

```bash
curl 'https://api.upload-post.com/api/uploadposts/reddit/detailed-posts/?profile_username=mi_perfil' \
--header 'Authorization: Bearer <YOUR_JWT_TOKEN>'
```

**Example Successful Response (200 OK):**

```json
{
  "posts": [
    {
      "id": "1abc123",
      "title": "Título del post",
      "subreddit": "programming",
      "body": "Contenido del post o URL si es un link post",
      "likes": 150,
      "comments": 42,
      "impressions": 1500,
      "has_image": true,
      "has_video": false,
      "media": [
        {
          "type": "image",
          "url": "https://i.redd.it/example.jpg",
          "width": 1920,
          "height": 1080
        }
      ],
      "url": "https://www.reddit.com/r/programming/comments/1abc123/titulo_del_post/",
      "created_at": "2026-01-15T10:30:00+00:00",
      "thumbnail": "https://b.thumbs.redditmedia.com/example.jpg"
    }
  ]
}
```

**Response Fields:**

| Field         | Type     | Description                                                   |
| ------------- | -------- | ------------------------------------------------------------- |
| `id`          | string   | Unique identifier of the Reddit post.                         |
| `title`       | string   | Title of the post.                                            |
| `subreddit`   | string   | Name of the subreddit where the post was published.           |
| `body`        | string   | Content of the post or URL if it's a link post.               |
| `likes`       | integer  | Number of upvotes on the post.                                |
| `comments`    | integer  | Number of comments on the post.                               |
| `impressions` | integer  | Number of views (uses `view_count` if available, otherwise `score`). |
| `has_image`   | boolean  | Indicates if the post contains images.                        |
| `has_video`   | boolean  | Indicates if the post contains video.                         |
| `media`       | array    | Array of media objects attached to the post.                  |
| `url`         | string   | Direct URL to the post on Reddit.                             |
| `created_at`  | string   | ISO 8601 timestamp of when the post was created.              |
| `thumbnail`   | string   | URL of the post's thumbnail image.                            |

**Media Types:**

| Type             | Description                      | Additional Fields               |
| ---------------- | -------------------------------- | ------------------------------- |
| `image`          | Individual or gallery image      | `width`, `height`               |
| `video`          | Video hosted on Reddit           | `width`, `height`, `duration`   |
| `external_video` | External video (YouTube, etc.)   | `thumbnail`, `provider`         |

**Error Responses:**

| Code | Message                                              | Description                                      |
| ---- | ---------------------------------------------------- | ------------------------------------------------ |
| 400  | `Query parameter "profile_username" is required.`   | The required parameter is missing.               |
| 400  | `Profile 'X' has no Reddit account connected.`      | The profile does not have a linked Reddit account. |
| 400  | `Reddit account 'X' not found.`                     | The Reddit account does not exist for the user.  |
| 500  | `An internal server error occurred.`                | Internal server error.                           |

**Notes:**

*   Supports automatic pagination, fetching up to 2000 posts (20 pages × 100 posts).
*   Gallery posts include all images in the `media` array.
*   Image URLs are already unescaped (no `&amp;`).
*   `impressions` uses `view_count` if available, otherwise uses `score` as a fallback.


---
# Google Business Reviews
URL: https://docs.upload-post.com/api/google-business-reviews

Read and reply to reviews on a connected Google Business Profile location. Both endpoints call the Google Business Profile API in real-time and refresh the stored access token automatically. Connect the Google account via OAuth first (the same connection used for Google Business posting — no extra permission is required).

## List Reviews

-   **Method:** `GET`
-   **Endpoint:** `/api/uploadposts/google-business/reviews`
-   **Authentication:** API Key — `Authorization: Apikey <YOUR_API_KEY>`

### Query Parameters

| Parameter     | Type    | Required | Description                                                                                             |
| :------------ | :------ | :------- | :------------------------------------------------------------------------------------------------------ |
| `user`        | string  | Yes      | The profile `username` that owns the connected Google Business account.                                  |
| `location_id` | string  | No       | Location to read, e.g. `locations/222222222` or a full `accounts/.../locations/...`. Defaults to the account's selected location. |
| `pageSize`    | integer | No       | Max reviews per page.                                                                                    |
| `pageToken`   | string  | No       | Pagination cursor from a previous response (`nextPageToken`).                                            |
| `orderBy`     | string  | No       | Sort order, e.g. `updateTime desc` or `rating desc`.                                                     |

### Example Request

```bash
curl 'https://api.upload-post.com/api/uploadposts/google-business/reviews?user=my-profile&pageSize=20' \
  -H 'Authorization: Apikey YOUR_API_KEY'
```

### Successful Response (`200 OK`)

```json
{
  "success": true,
  "reviews": [
    {
      "name": "accounts/123456789/locations/222222222/reviews/AbcDef",
      "reviewer": { "displayName": "Jane Doe" },
      "starRating": "FIVE",
      "comment": "Great service!",
      "createTime": "2026-07-01T10:00:00Z",
      "updateTime": "2026-07-01T10:00:00Z",
      "reviewReply": { "comment": "Thanks Jane!", "updateTime": "2026-07-02T09:00:00Z" }
    }
  ],
  "averageRating": 4.7,
  "totalReviewCount": 128,
  "nextPageToken": "..."
}
```

| Field              | Description                                                        |
| :----------------- | :----------------------------------------------------------------- |
| `reviews`          | Array of reviews. Each review's `name` is the full resource path used to reply. |
| `averageRating`    | Average star rating for the location.                              |
| `totalReviewCount` | Total number of reviews for the location.                          |
| `nextPageToken`    | Present when more reviews are available; pass it as `pageToken`.   |

---

## Reply to a Review

Creates the owner reply to a review, or updates it if one already exists.

-   **Method:** `PUT` (also accepts `POST`)
-   **Endpoint:** `/api/uploadposts/google-business/reviews/reply`
-   **Authentication:** API Key — `Authorization: Apikey <YOUR_API_KEY>`

### Body Parameters (JSON)

| Name          | Type   | Required | Description                                                                                          |
| :------------ | :----- | :------- | :--------------------------------------------------------------------------------------------------- |
| `user`        | String | Yes      | The profile `username` that owns the connected Google Business account.                              |
| `comment`     | String | Yes      | The public reply text.                                                                               |
| `review_name` | String | No\*     | Full review resource path from **List Reviews** (`accounts/.../locations/.../reviews/{id}`).          |
| `review_id`   | String | No\*     | Review ID. Requires `location_id` to build the resource path.                                        |
| `location_id` | String | No       | Location for the review when using `review_id`.                                                       |

\* Provide either `review_name` (preferred) or `review_id` + `location_id`.

### Example Request

```bash
curl -X PUT https://api.upload-post.com/api/uploadposts/google-business/reviews/reply \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "user": "my-profile",
    "review_name": "accounts/123456789/locations/222222222/reviews/AbcDef",
    "comment": "Thank you for the kind words — see you again soon!"
  }'
```

### Successful Response (`200 OK`)

```json
{
  "success": true,
  "reply": { "comment": "Thank you for the kind words — see you again soon!", "updateTime": "2026-07-18T12:00:00Z" }
}
```

> **Notes:**
> - Reviews and replies use the Google Business Profile v4 API. Only **verified** locations can receive replies.
> - Reviews cannot be deleted via the API, and star ratings cannot be changed — you can only reply.
> - Token refresh is handled automatically; connect the Google account via OAuth first.


---
# Comments
URL: https://docs.upload-post.com/api/instagram-comments

# Comments

Retrieve comments from social media posts, send private replies (DMs) to commenters, or post public replies visible under the original comment.

---

## Get Post Comments

Retrieve all comments on a specific post. Accepts either a numeric media ID or a full post URL.

### Endpoint

```
GET /api/uploadposts/comments
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Query Parameters

| Name     | Type    | Required | Description                                                                                                                    |
|----------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------|
| platform | String  | Yes      | The platform to retrieve comments from (e.g., `"instagram"`).                                                                  |
| user     | String  | Yes      | Profile username (as configured in Upload-Post).                                                                               |
| post_id  | String  | Yes\*    | Numeric media ID. Use `post_id` or `post_url` (one is required).                                                               |
| post_url | String  | Yes\*    | Full post URL (e.g., `https://www.instagram.com/p/ABC123/`). Alternative to `post_id`.                                         |
| limit    | Integer | No       | Comments per page, between **1 and 50** (Meta's hard cap for this edge). If omitted, Instagram's default applies (~25).        |
| after    | String  | No       | Cursor returned by Meta in the previous page (`pagination.next_cursor`). Use it to fetch the next page.                        |

> **Ordering:** Meta returns comments newest-first (reverse-chronological) on Graph API v3.2 and later. There is no query parameter to change this — the order is fixed by Meta.

### Example Requests

**Single page (default):**

```bash
curl 'https://api.upload-post.com/api/uploadposts/comments?platform=instagram&user=my-profile&post_url=https://www.instagram.com/p/ABC123/' \
  -H 'Authorization: Apikey your-api-key-here'
```

**Paginate through all comments (50 per page):**

```bash
# First page
curl 'https://api.upload-post.com/api/uploadposts/comments?platform=instagram&user=my-profile&post_id=17890455123456789&limit=50' \
  -H 'Authorization: Apikey your-api-key-here'

# Next page — pass back the next_cursor from the previous response
curl 'https://api.upload-post.com/api/uploadposts/comments?platform=instagram&user=my-profile&post_id=17890455123456789&limit=50&after=QVFIUm...' \
  -H 'Authorization: Apikey your-api-key-here'
```

Loop while `pagination.has_next` is `true`, passing `pagination.next_cursor` as `after` each time.

### Responses

- **200 OK**

```json
{
  "success": true,
  "comments": [
    {
      "id": "17858893269123456",
      "text": "Great post!",
      "timestamp": "2025-06-15T10:30:00+0000",
      "user": {
        "id": "17841400123456789",
        "username": "commenter_user"
      }
    },
    {
      "id": "17858893269789012",
      "text": "Love this content",
      "timestamp": "2025-06-15T11:00:00+0000",
      "user": {
        "id": "17841400987654321",
        "username": "another_user"
      }
    }
  ],
  "pagination": {
    "next_cursor": "QVFIUm9TbGd...",
    "has_next": true
  }
}
```

When the last page is reached, `pagination` is `{"next_cursor": null, "has_next": false}`.

- **400 Bad Request**

```json
{
  "success": false,
  "error": "Missing required query parameters: platform, user, and either post_id or post_url"
}
```

- **400 Bad Request** (invalid `limit`)

```json
{
  "success": false,
  "error": "Query parameter 'limit' must be between 1 and 50 (Meta's cap for this edge)."
}
```

- **400 Bad Request** (invalid post URL)

```json
{
  "success": false,
  "error": "Could not find media ID for the provided URL. Make sure the post belongs to the authenticated account."
}
```

- **500 Internal Server Error**

```json
{
  "success": false,
  "error": "An internal server error occurred."
}
```

> **Note:** When using a post URL, the API automatically resolves the shortcode to a media ID by scanning the account's recent posts. The resolved IDs are cached for subsequent requests. The post must belong to the authenticated account.

> **Rate limiting:** Calls to this endpoint are subject to your account's [global API rate limits](../guides/rate-limits.md), which scale with your plan. There is no per-post throttle, so you can walk all pages of a viral post without artificial waits.

---

## Reply to Comment (Private Reply)

Send a private reply (DM) to the author of a comment on your post. This sends a direct message to the commenter.

### Endpoint

```
POST /api/uploadposts/comments/reply
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name       | Type   | Required | Description                                                  |
|------------|--------|----------|--------------------------------------------------------------|
| platform   | String | Yes      | The platform (e.g., `"instagram"`).                          |
| user       | String | Yes      | Profile username (as configured in Upload-Post).             |
| comment_id | String | Yes      | The ID of the comment to reply to (from Get Post Comments).  |
| message    | String | Yes      | The private reply message text.                              |
| buttons    | Array  | No       | Up to 3 `web_url` buttons rendered in the Instagram DM. Each item is an object `{title, url}` (title max 20 chars, url must be http/https). |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/comments/reply \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "instagram",
    "user": "my-profile",
    "comment_id": "17858893269123456",
    "message": "Thanks for your comment! Check your DMs for more info.",
    "buttons": [
      { "title": "Learn more", "url": "https://example.com" }
    ]
  }'
```

### Responses

- **200 OK** (reply sent successfully)

```json
{
  "success": true,
  "recipient_id": "17841400123456789",
  "message_id": "aWdGM...",
  "message": "Private reply sent successfully"
}
```

- **400 Bad Request** (missing fields)

```json
{
  "success": false,
  "error": "Missing required fields: platform, user, comment_id, message"
}
```

- **429 Too Many Requests** (daily DM limit exceeded)

```json
{
  "success": false,
  "error": "Daily DM limit exceeded."
}
```

- **500 Internal Server Error**

```json
{
  "success": false,
  "error": "An internal server error occurred."
}
```

---

## Reply to Comment (Public Reply)

Post a public reply to a comment on your Instagram post. The reply appears as a visible comment under the original comment.

### Endpoint

```
POST /api/uploadposts/comments/public-reply
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name       | Type   | Required | Description                                                  |
|------------|--------|----------|--------------------------------------------------------------|
| platform   | String | Yes      | The platform (e.g., `"instagram"`).                          |
| user       | String | Yes      | Profile username (as configured in Upload-Post).             |
| comment_id | String | Yes      | The ID of the comment to reply to (from Get Post Comments).  |
| message    | String | Yes      | The public reply message text.                               |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/comments/public-reply \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "instagram",
    "user": "my-profile",
    "comment_id": "17858893269123456",
    "message": "Thank you! Glad you liked it."
  }'
```

### Responses

- **200 OK** (reply posted successfully)

```json
{
  "success": true,
  "id": "17858893269654321",
  "message": "Instagram public reply posted successfully"
}
```

- **400 Bad Request** (missing fields)

```json
{
  "success": false,
  "error": "Missing required fields: platform, user, comment_id, message"
}
```

- **429 Too Many Requests** (daily limit exceeded)

```json
{
  "success": false,
  "error": "Daily DM limit exceeded."
}
```

- **500 Internal Server Error**

```json
{
  "success": false,
  "error": "An internal server error occurred."
}
```

---

## Important Notes

1. **7-day window for private replies**: Some platforms only allow private replies to recent comments (e.g., Instagram requires comments less than 7 days old). Public replies do not have this restriction.

2. **Comment must be on your post**: You can only reply to comments on posts owned by the authenticated account.

3. **Daily limits**: Upload-Post enforces a configurable daily limit per user. Both private and public replies count toward this limit. When exceeded, the API returns a `429` status code.

4. **Public vs. private replies**: Use `/comments/reply` to send a private DM to the comment author. Use `/comments/public-reply` to post a visible reply under the original comment. Both require the `comment_id` from the Get Post Comments endpoint.

5. **Using comment data for DMs**: Each comment includes the commenter's user ID. You can use this ID with the [Direct Messages](./instagram-dms.md) endpoint to send follow-up DMs directly.


---
# Direct Messages
URL: https://docs.upload-post.com/api/instagram-dms

# Direct Messages

Send direct messages (DMs) and retrieve conversations on connected social media accounts.

---

## Send a Direct Message

Send a DM directly to a user using their platform-specific User ID.

### Endpoint

```
POST /api/uploadposts/dms/send
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Body Parameters (JSON)

| Name         | Type   | Required | Description                                                                 |
|--------------|--------|----------|-----------------------------------------------------------------------------|
| platform     | String | Yes      | The platform to send the DM on (e.g., `"instagram"`).                      |
| user         | String | Yes      | Profile username (as configured in Upload-Post).                            |
| recipient_id | String | Yes      | The platform-specific User ID of the recipient.                            |
| message      | String | Yes      | The text message to send.                                                   |
| buttons      | Array  | No       | Up to 3 `web_url` buttons rendered in the Instagram DM. Each item is an object `{title, url}` (title max 20 chars, url must be http/https). |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/dms/send \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "instagram",
    "user": "my-profile",
    "recipient_id": "17841400123456789",
    "message": "Hello! Thanks for reaching out.",
    "buttons": [
      { "title": "Visit site", "url": "https://example.com" }
    ]
  }'
```

### Responses

- **200 OK** (DM sent successfully)

```json
{
  "success": true,
  "recipient_id": "17841400123456789",
  "message_id": "aWdGM...",
  "message": "DM sent successfully"
}
```

- **400 Bad Request** (missing fields, invalid account)

```json
{
  "success": false,
  "error": "Missing required fields: platform, user, recipient_id, message"
}
```

- **429 Too Many Requests** (daily DM limit exceeded)

```json
{
  "success": false,
  "error": "Daily DM limit exceeded."
}
```

- **500 Internal Server Error**

```json
{
  "success": false,
  "error": "An internal server error occurred."
}
```

---

## Get Conversations

Retrieve the list of DM conversations for an account, including participants and recent messages.

### Endpoint

```
GET /api/uploadposts/dms/conversations
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Query Parameters

| Name     | Type   | Required | Description                                                      |
|----------|--------|----------|------------------------------------------------------------------|
| platform | String | Yes      | The platform to retrieve conversations from (e.g., `"instagram"`). |
| user     | String | Yes      | Profile username (as configured in Upload-Post).                 |

### Example Request

```bash
curl 'https://api.upload-post.com/api/uploadposts/dms/conversations?platform=instagram&user=my-profile' \
  -H 'Authorization: Apikey your-api-key-here'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "conversations": [
    {
      "id": "t_123456789",
      "participants": {
        "data": [
          { "id": "17841400123456789", "username": "user_one" },
          { "id": "17841400987654321", "username": "user_two" }
        ]
      },
      "messages": {
        "data": [
          {
            "id": "aWdGM...",
            "created_time": "2025-01-15T10:30:00+0000",
            "from": { "id": "17841400123456789", "username": "user_one" },
            "to": { "data": [{ "id": "17841400987654321", "username": "user_two" }] },
            "message": "Hey, thanks for the info!"
          }
        ]
      }
    }
  ]
}
```

- **400 Bad Request**

```json
{
  "success": false,
  "error": "Missing required query parameters: platform, user"
}
```

- **500 Internal Server Error**

```json
{
  "success": false,
  "error": "An internal server error occurred."
}
```

---

## Important Notes

1. **Messaging policies vary by platform**: Each platform has its own messaging rules. For example, Instagram requires the recipient to have messaged your account first (24-hour window).

2. **Daily DM limits**: Upload-Post enforces a configurable daily DM limit per user to prevent accidental overuse. When the limit is reached, the API returns a `429` status code.

### Where to find the recipient ID

The recipient's User ID can be obtained from:

- The **Get Conversations** endpoint above (each participant has an `id` field).
- The **Get Post Comments** endpoint (`GET /api/uploadposts/comments`) where each comment includes the commenter's `user.id`.

### Difference from Comment Replies

| Feature | Comment Reply (`/comments/reply`) | Direct Message (`/dms/send`) |
|---|---|---|
| Recipient | `comment_id` — replies to a specific comment | `recipient_id` — sends to a user by ID |
| Use case | Auto-reply to post engagement | Customer support, follow-up conversations |


---
# Media List
URL: https://docs.upload-post.com/api/instagram-media

# Media List

Retrieve a list of recent media (posts, reels, videos, pins, tweets, etc.) from a connected social media account. Supports all major platforms: Instagram, TikTok, YouTube, LinkedIn, Facebook, X (Twitter), Threads, Pinterest, Bluesky, and Reddit.

Useful for building post selectors, displaying recent content, or getting media IDs for other API calls.

---

## Get User Media

### Endpoint

```
GET /api/uploadposts/media
```

### Headers

| Name          | Value                    | Description                     |
|---------------|--------------------------|---------------------------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Query Parameters

| Name     | Type   | Required | Description                                                      |
|----------|--------|----------|------------------------------------------------------------------|
| platform | String | Yes      | The platform to retrieve media from. See [Supported Platforms](#supported-platforms). |
| user     | String | Yes      | Profile username (as configured in Upload-Post).                 |
| limit    | Integer | No      | Number of media items to return. Defaults to `25`, clamped to the range `1`–`100`, and further capped per platform. See [Pagination](#pagination). |
| cursor   | String | No       | Opaque next-page token returned by the previous response (`pagination.next_cursor`). Not supported on LinkedIn, Discord and Telegram. See [Pagination](#pagination). |
| page_urn | String | No       | **LinkedIn only.** Selects which LinkedIn page to fetch posts from. Accepts a numeric organization ID (e.g., `12345`), a full URN (e.g., `urn:li:organization:12345`), or `me` to force the connected member's personal profile. If omitted, the endpoint targets the page that was active when the account was linked — for accounts connected as an organization admin, the first administered organization is auto-resolved; otherwise the personal profile is used. Use the [LinkedIn Pages](/api/get-linkedin-pages) endpoint to list available organizations. |

### Supported Platforms

| Platform    | Value        | Description                                |
|-------------|--------------|--------------------------------------------|
| Instagram   | `instagram`  | Posts, reels, and carousels                |
| TikTok      | `tiktok`     | Videos                                     |
| YouTube     | `youtube`    | Videos from the channel's uploads playlist |
| LinkedIn    | `linkedin`   | Posts (text, images, articles)             |
| Facebook    | `facebook`   | Page/profile posts                         |
| X (Twitter) | `x`          | Tweets with media information              |
| Threads     | `threads`    | Thread posts                               |
| Pinterest   | `pinterest`  | Pins                                       |
| Bluesky     | `bluesky`    | Posts (skeets)                             |
| Reddit      | `reddit`     | Submissions                                |

### Example Requests

**Instagram:**
```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=instagram&user=my-profile' \
  -H 'Authorization: Apikey your-api-key-here'
```

**TikTok:**
```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=tiktok&user=my-profile' \
  -H 'Authorization: Apikey your-api-key-here'
```

**YouTube:**
```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=youtube&user=my-profile' \
  -H 'Authorization: Apikey your-api-key-here'
```

**LinkedIn (personal profile):**
```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=linkedin&user=my-profile' \
  -H 'Authorization: Apikey your-api-key-here'
```

**LinkedIn (organization page):**
```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=linkedin&user=my-profile&page_urn=12345' \
  -H 'Authorization: Apikey your-api-key-here'
```

**LinkedIn (force the personal profile of an account connected as an org admin):**
```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=linkedin&user=my-profile&page_urn=me' \
  -H 'Authorization: Apikey your-api-key-here'
```

### Responses

- **200 OK**

```json
{
  "success": true,
  "media": [
    {
      "id": "17890455123456789",
      "caption": "Check out our latest product launch!",
      "media_type": "IMAGE",
      "media_url": "https://scontent.xx.fbcdn.net/v/image123.jpg",
      "permalink": "https://www.instagram.com/p/ABC123/",
      "timestamp": "2025-06-15T10:30:00+0000",
      "thumbnail_url": null
    },
    {
      "id": "17890455987654321",
      "caption": "Behind the scenes",
      "media_type": "VIDEO",
      "media_url": "https://scontent.xx.fbcdn.net/v/video456.mp4",
      "permalink": "https://www.instagram.com/reel/DEF456/",
      "timestamp": "2025-06-14T15:00:00+0000",
      "thumbnail_url": "https://scontent.xx.fbcdn.net/..."
    },
    {
      "id": "17890455111222333",
      "caption": "Photo dump",
      "media_type": "CAROUSEL_ALBUM",
      "media_url": null,
      "permalink": "https://www.instagram.com/p/GHI789/",
      "timestamp": "2025-06-13T09:00:00+0000",
      "thumbnail_url": null
    }
  ],
  "pagination": {
    "limit": 50,
    "next_cursor": "QVFIUkc...",
    "has_more": true
  }
}
```

The `pagination` object is additive — every pre-existing key of the response (`success`, `media`, and the per-item fields) is unchanged, so existing integrations keep working without any modification.

- **400 Bad Request**

```json
{
  "success": false,
  "error": "Platform parameter is required"
}
```

```json
{
  "success": false,
  "error": "Pagination cursor is not supported for LinkedIn"
}
```

- **500 Internal Server Error**

```json
{
  "success": false,
  "error": "An internal server error occurred."
}
```

### Response Fields

All platforms return media items with a consistent structure:

| Field           | Type        | Description                                              |
|-----------------|-------------|----------------------------------------------------------|
| `id`            | String      | Platform-specific unique identifier for the media item   |
| `caption`       | String      | Text content, caption, or title of the post              |
| `media_type`    | String      | Type of media. See [Media Types](#media-types)           |
| `media_url`     | String/null | Direct URL to the media file (image or video). See [Media URL Availability](#media-url-availability) |
| `permalink`     | String/null | Direct URL to the post on the platform                   |
| `timestamp`     | String/null | ISO 8601 timestamp of when the post was created          |
| `thumbnail_url` | String/null | URL of the thumbnail/preview image (if available)        |

| Field        | Type    | Description                                                                 |
|--------------|---------|-----------------------------------------------------------------------------|
| `pagination` | Object  | Page metadata. See [Pagination](#pagination).                               |

### Pagination

Use `limit` and `cursor` to page through an account's media instead of receiving only the most recent items.

| Name     | Type    | Default | Description                                                                                       |
|----------|---------|---------|---------------------------------------------------------------------------------------------------|
| `limit`  | Integer | `25`    | How many items to return. Values are clamped to `1`–`100`, then capped again by the platform's own maximum page size (see the table below). |
| `cursor` | String  | –       | Opaque token identifying the next page. Pass back exactly what the previous response returned in `pagination.next_cursor`. Do not build, parse or store these tokens — they are the platform's own paging tokens and their format can change at any time. |

Every response carries a `pagination` object:

```json
{
  "limit": 50,
  "next_cursor": "QVFIUkc...",
  "has_more": true
}
```

| Field         | Type        | Description                                                                 |
|---------------|-------------|-----------------------------------------------------------------------------|
| `limit`       | Integer     | The effective limit that was applied after clamping and per-platform capping. |
| `next_cursor` | String/null | Token to pass as `cursor` on the next request. `null` when there are no more pages. |
| `has_more`    | Boolean     | `true` when another page is available.                                       |

On the last page the object looks like this:

```json
{
  "limit": 50,
  "next_cursor": null,
  "has_more": false
}
```

#### Per-Platform Limits and Cursor Support

| Platform    | Max `limit` | Cursor support | Notes                                              |
|-------------|-------------|----------------|----------------------------------------------------|
| Instagram   | 100         | Yes            | Graph API `after` cursor                           |
| Facebook    | 100         | Yes            | Graph API `after` cursor                           |
| Threads     | 100         | Yes            | Graph API `after` cursor                           |
| TikTok      | 20          | Yes            | TikTok caps its page size at 20 items              |
| YouTube     | 50          | Yes            | YouTube caps its page size at 50 items             |
| Bluesky     | 100         | Yes            |                                                    |
| X (Twitter) | 100         | Yes            |                                                    |
| Pinterest   | 100         | Yes            |                                                    |
| Reddit      | 100         | Yes            |                                                    |
| LinkedIn    | 100         | **No**         | `limit` only. Passing `cursor` returns `400`.      |
| Discord     | –           | **No**         | `limit` only. Passing `cursor` returns `400`.      |
| Telegram    | –           | **No**         | `limit` only. Passing `cursor` returns `400`.      |

:::warning
**LinkedIn, Discord and Telegram do not support cursors.** Their APIs do not expose a paging token, so these platforms honour `limit` only. Sending a `cursor` for one of them returns `400 Bad Request` with a message naming the platform (e.g. `"Pagination cursor is not supported for LinkedIn"`), rather than silently ignoring it.
:::

#### Requesting a Specific Page Size

```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=instagram&user=my-profile&limit=50' \
  -H 'Authorization: Apikey your-api-key-here'
```

#### Requesting the Next Page

```bash
curl 'https://api.upload-post.com/api/uploadposts/media?platform=instagram&user=my-profile&limit=50&cursor=QVFIUkc...' \
  -H 'Authorization: Apikey your-api-key-here'
```

#### Paginating Through Everything

Keep calling the endpoint with the `next_cursor` from the previous response until `has_more` is `false`.

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
API_KEY="your-api-key-here"
CURSOR=""

while :; do
  URL="https://api.upload-post.com/api/uploadposts/media?platform=instagram&user=my-profile&limit=100"
  [ -n "$CURSOR" ] && URL="$URL&cursor=$CURSOR"

  RESPONSE=$(curl -s "$URL" -H "Authorization: Apikey $API_KEY")
  echo "$RESPONSE" | jq -r '.media[].id'

  [ "$(echo "$RESPONSE" | jq -r '.pagination.has_more')" = "true" ] || break
  CURSOR=$(echo "$RESPONSE" | jq -r '.pagination.next_cursor')
done
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

API_KEY = "your-api-key-here"
all_media = []
cursor = None

while True:
    params = {"platform": "instagram", "user": "my-profile", "limit": 100}
    if cursor:
        params["cursor"] = cursor

    response = requests.get(
        "https://api.upload-post.com/api/uploadposts/media",
        headers={"Authorization": f"Apikey {API_KEY}"},
        params=params,
    )
    data = response.json()
    all_media.extend(data["media"])

    pagination = data.get("pagination", {})
    if not pagination.get("has_more"):
        break
    cursor = pagination["next_cursor"]

print(f"Fetched {len(all_media)} media items")
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript
const API_KEY = "your-api-key-here";
const allMedia = [];
let cursor = null;

while (true) {
  const params = new URLSearchParams({
    platform: "instagram",
    user: "my-profile",
    limit: "100",
  });
  if (cursor) params.set("cursor", cursor);

  const response = await fetch(
    `https://api.upload-post.com/api/uploadposts/media?${params}`,
    { headers: { Authorization: `Apikey ${API_KEY}` } }
  );
  const data = await response.json();
  allMedia.push(...data.media);

  if (!data.pagination?.has_more) break;
  cursor = data.pagination.next_cursor;
}

console.log(`Fetched ${allMedia.length} media items`);
```

</TabItem>
</Tabs>

:::tip
For LinkedIn, Discord and Telegram there is no second page: request the largest `limit` you need in a single call and stop.
:::

### Media Types

| Type             | Description                                    |
|------------------|------------------------------------------------|
| `IMAGE`          | Single photo post or image pin                 |
| `VIDEO`          | Video post, reel, or video pin                 |
| `CAROUSEL_ALBUM` | Multi-image/video post (carousel)              |
| `TEXT`           | Text-only post (no media attached)             |

### Media URL Availability

The `media_url` field returns a direct URL to the media file (image or video) when available. Support varies by platform:

| Platform    | `media_url` Support | Details                                                  |
|-------------|---------------------|----------------------------------------------------------|
| Instagram   | Yes                 | Direct image/video URL. Not available for CAROUSEL_ALBUM parent (use children). May be omitted for copyrighted content. URLs are temporary. |
| Threads     | Yes                 | Direct image/video URL, same behavior as Instagram.       |
| Facebook    | Yes                 | Image URL or playable video URL via attachments.          |
| X (Twitter) | Yes                 | Direct photo URL. For videos, returns the preview image URL. |
| LinkedIn    | Yes                 | Resolved via Images/Videos API. URLs are signed and temporary. |
| Reddit      | Yes                 | Direct `i.redd.it` image URL, `v.redd.it` video URL (video-only, no audio), or first gallery image URL. |
| Bluesky     | Yes                 | `fullsize` CDN image (up to 2000px) or HLS playlist URL (`.m3u8`) for videos. |
| Pinterest   | Yes                 | Largest available image URL (up to 1200px or original). Video URLs are restricted by Pinterest. |
| TikTok      | No                  | TikTok API does not expose direct video file URLs. Use `permalink` instead. |
| YouTube     | No                  | YouTube API does not provide direct video URLs (prohibited by ToS). Use `permalink` instead. |

:::warning
Media URLs from most platforms are **temporary** and will expire after some time (hours to days). Do not store them permanently — re-fetch from the API when needed, or download the media file to your own storage.
:::

### Common Use Cases

- **Post selector UI**: Display the user's recent posts so they can pick one for comment monitoring or AutoDMs.
- **Get media IDs**: Use the `id` field from the response as the `post_id` parameter in the [Comments](./instagram-comments.md) endpoint.
- **Content overview**: Show a dashboard of recent content across all platforms with permalinks and captions.
- **Cross-platform analytics**: Aggregate media from multiple platforms to display a unified content calendar.
- **Full back-catalogue sync**: Use [`limit` and `cursor`](#pagination) to walk an account's entire media history, then feed the resulting IDs into [Cached Post Analytics](./get-analytics.md#get-apiuploadpostspost-analyticscached) for bulk metrics.


---
# Upload-Post API Overview
URL: https://docs.upload-post.com/api/overview

# Upload-Post API Overview

Upload-Post provides a simple and powerful API for uploading content to TikTok, Instagram, Bluesky, LinkedIn, YouTube, Facebook, X (Twitter), Threads, Pinterest, Reddit, Discord, Telegram, and Google Business Profile. This documentation will help you get started with our API and make the most of our services.

## Getting Started

1. Create an account at [upload-post.com](https://www.upload-post.com)
2. Connect your TikTok, Instagram, Bluesky, LinkedIn, YouTube, Facebook, X (Twitter), Threads, Pinterest, Reddit, Discord, Telegram, and Google Business Profile accounts
3. Generate your API key from the dashboard
4. Start making API calls

## Authentication

All API requests require authentication using an API key. Include your API key in the request header:

```bash
Authorization: Apikey your-api-key-here
```

## Rate Limits

- Free tier: 10 uploads per month
- Additional uploads available through paid plans

## Base URL

All API endpoints are available at:
```
https://api.upload-post.com/api
```

For detailed information about each endpoint, check out our [API Reference](./reference).


---
# Photo Format Requirements
URL: https://docs.upload-post.com/api/photo-requirements

# Photo Format Requirements

This document outlines the photo format requirements for uploading to various social media platforms via the API. For platforms where specific requirements are not listed, standard image formats like JPEG and PNG are generally accepted. However, for the most accurate and up-to-date information, please consult the official documentation of each respective platform.

## Threads Photo Requirements

- **Format:** JPEG, PNG
- **File Size:** 8 MB maximum
- **Aspect Ratio:** Limit: 10:1 (e.g., can be from 1:10 to 10:1)
- **Width:**
  - Minimum: 320px (images narrower than 320px will be scaled up to 320px)
  - Maximum: 1440px (images wider than 1440px will be scaled down to 1440px)
- **Color Space:** sRGB (images with other color spaces will be converted to sRGB)
- **Items Per Post:** Up to 10 media items per post (carousel). If you provide more than 10 items, they are automatically distributed across multiple posts (up to 10 items per post).
- **Thread Media Layout:** Use the `threads_thread_media_layout` parameter to control how media items are distributed across posts. For example, `"5,5"` splits 10 items into 2 posts of 5 each.

## Instagram Photo Requirements

- **Media Type:** The API supports "IMAGE" for feed posts and "STORIES".
- **General Guidance:** Instagram supports: png, jpeg, gif formats.
- For detailed specifications (resolution, aspect ratio, file size), please refer to the official Instagram documentation.

## TikTok Photo Requirements

- While the API allows photo uploads to TikTok (e.g., for slideshows with `auto_add_music`), specific format requirements (resolution, aspect ratio, file size) are not detailed in the provided source.
- **General Guidance:** Only image formats: JPG, JPEG, or WEBP are compatible.
- Please refer to the official TikTok documentation for specific photo guidelines.

## Facebook Photo Requirements

- **General Guidance:** Facebook supports various image formats, including JPEG, PNG, GIF, and WebP.
- The API `upload-photo.md` documentation notes that the `description` is applied only to the first photo uploaded.
- For detailed specifications, please refer to the official Facebook documentation.

## X (Twitter) Photo Requirements

- **General Guidance:** X (Twitter) supports JPEG, PNG, GIF, and WEBP formats.
- **Max File Size:** 5 MB per image
- **Images Per Tweet:** Up to 4 images per tweet. If you provide more than 4 images, they are automatically distributed across a thread (up to 4 images per tweet).
- **Thread Image Layout:** Use the `x_thread_image_layout` parameter to control how images are distributed across tweets in the thread. For example, `"4,4"` puts 4 images in each of 2 tweets.
- For detailed specifications, please refer to the official X/Twitter documentation.

## LinkedIn Photo Requirements

- **General Guidance:** LinkedIn supports JPEG, PNG, and GIF formats.
- The API `upload-photo.md` common parameters apply. The caption is used as post commentary.
- For detailed specifications, please refer to the official LinkedIn documentation.

## Pinterest Photo Requirements

- **Max Image Size:** 20 MB
- **Supported Formats:** BMP, JPEG, PNG, TIFF, GIF, Animated GIF, WEBP
- **Recommended Size:** 1000 x 1500 px
- **Aspect Ratio:** 2:3
- **Minimum Size:** 600 x 900 px
- **Maximum Size:** 2000 x 3000 px
- **Content-Type:** A valid media Content-Type such as image/jpeg, image/png, or image/webp returned by the hosting provider
- **Image Carousel:**
  - Up to five carousel images
  - Images must be the same dimension

## Reddit Photo Requirements

- **Max Image Size:** 10 MB
- **Supported Formats:** JPG, PNG, GIF, WEBP

## Bluesky Photo Requirements

- **Max Images:** 4 per post
- **Max File Size:** 1 MB per image
- **Supported Formats:** JPEG, PNG, GIF, WEBP
- **Alt Text:** Supported and recommended
- **Daily Limit:** 50 uploads per day (combined photos and videos)

## Discord Photo Requirements

- **Max Images:** 10 per message (sent as attachments)
- **Supported Formats:** JPG, PNG, GIF, WEBP (any image type Discord accepts)
- **Caption:** Up to 2,000 characters, applied as the message content
- **Max File Size:** Bounded by the upload limit of the target Discord server (the default Discord per-attachment limit applies)

## Telegram Photo Requirements

- **Max Images:** 10 per album (sent via `sendMediaGroup`; a single photo is sent via `sendPhoto`)
- **Supported Formats:** JPG, PNG, WEBP (any image type Telegram accepts)
- **Caption:** Up to 1,024 characters, applied to the first item of the album

---
*Note: The information for Instagram, TikTok, Facebook, X (Twitter), and LinkedIn photo requirements above is general. The provided source code focused primarily on video specifications and Threads image specifications. Always check the official platform guidelines for the latest and most precise requirements.*


---
# Retry & Unpublish
URL: https://docs.upload-post.com/api/post-actions

# Retry & Unpublish

Post-publish actions on an upload: **retry** a failed upload without re-uploading media, or **unpublish** (delete) a post that was already published to a platform.

---

## Retry a Failed Upload

Re-enqueue an upload that failed on one or more platforms. Only the platforms that failed are retried; platforms that already succeeded are left untouched. The original media snapshot is reused, so **you do not need to re-upload the file**.

### Endpoint

```
POST /api/uploadposts/posts/retry
```

### Headers

| Name          | Value                    | Description                     |
| :------------ | :----------------------- | :------------------------------ |
| Authorization | Apikey your-api-key-here | Your API key for authentication |
| Content-Type  | application/json         | Request body format             |

### Body Parameters (JSON)

| Name         | Type   | Required | Description                                                              |
| :----------- | :----- | :------- | :---------------------------------------------------------------------- |
| `request_id` | String | Yes\*    | The `request_id` returned by the original async upload.                 |
| `job_id`     | String | Yes\*    | The scheduled job ID. Alternative to `request_id`.                      |

\* Provide either `request_id` or `job_id` (one is required).

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/posts/retry \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "request_id": "3f7c2b1a-9d4e-4a2b-8c1f-1234567890ab"
  }'
```

### Successful Response (`200 OK`)

```json
{
  "success": true,
  "request_id": "3f7c2b1a-9d4e-4a2b-8c1f-1234567890ab",
  "message": "Failed platforms re-enqueued for retry"
}
```

Poll the [upload status](./upload-status.md) endpoint with the `request_id` to track the retried platforms.

### Error Responses

- **400 Bad Request** — neither `request_id` nor `job_id` provided.
- **404 Not Found** — no upload found for the given identifier.
- **409 Conflict** — nothing to retry (no failed platforms).
- **500 Internal Server Error**

> **No re-upload required:** Retry reuses the media snapshot stored from the original upload. There is no need to send the file again.

---

## Unpublish (Delete) a Published Post

Delete a post that was already published to a platform. This removes the live post from the target network.

### Platform support

| Platform  | Deletion |
| :-------- | :------: |
| Facebook  |   ✅     |
| YouTube   |   ✅     |
| X         |   ✅     |
| LinkedIn  |   ✅     |
| Threads   |   ✅     |
| Instagram |   ❌     |
| TikTok    |   ❌     |

> **Instagram and TikTok do not support deletion via API.** These platforms have no public endpoint to delete a published post, so the API returns a clear error if you attempt it.

### Endpoint

```
POST /api/uploadposts/posts/unpublish
```

### Headers

| Name          | Value                    | Description                     |
| :------------ | :----------------------- | :------------------------------ |
| Authorization | Apikey your-api-key-here | Your API key for authentication |
| Content-Type  | application/json         | Request body format             |

### Body Parameters (JSON)

| Name       | Type   | Required | Description                                                              |
| :--------- | :----- | :------- | :---------------------------------------------------------------------- |
| `platform` | String | Yes      | One of `facebook`, `youtube`, `x`, `linkedin`, `threads`.               |
| `user`     | String | Yes      | Profile username (as configured in Upload-Post).                        |
| `post_id`  | String | Yes      | The published post's ID on the target platform.                         |

### Example Request

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/posts/unpublish \
  -H 'Authorization: Apikey your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "platform": "youtube",
    "user": "my-profile",
    "post_id": "dQw4w9WgXcQ"
  }'
```

### Successful Response (`200 OK`)

```json
{
  "success": true,
  "message": "Post deleted successfully"
}
```

### Error Responses

- **400 Bad Request** — missing fields, or an unsupported platform (e.g. `instagram`, `tiktok`).
- **403 Forbidden** — the connected account is not authorized to delete the post.
- **404 Not Found** — no post found for the given `post_id`.
- **500 Internal Server Error**


---
# Queue System
URL: https://docs.upload-post.com/api/queue-system

# Queue System

The queue system allows you to automatically schedule posts to predefined time slots. Instead of specifying an exact date/time with `scheduled_date`, you can use `add_to_queue=true` to have the system automatically assign your post to the next available slot.

## How It Works

The queue is **always active** with default time slots (9am, 12pm, 5pm Eastern Time). You can customize these slots, timezone, and active days through the Queue Settings endpoints.

When you upload content with `add_to_queue=true`:
1. The system finds the next available slot based on your queue configuration
2. Your post is automatically scheduled to that slot
3. You receive a `job_id` to track the scheduled post

### Multiple Posts Per Slot

By default, each slot accepts **1 post**. You can increase this with the `max_posts_per_slot` setting to allow multiple posts in the same time slot. This is useful when you want to post to different platforms at the same time (e.g., an Instagram post and a Facebook post both at 9am).

You can also **mark individual slots as full** to prevent new posts from being added, even if they haven't reached the maximum capacity.

---

## Using the Queue in Uploads

Add the `add_to_queue` parameter to any upload endpoint:

| Name | Type | Required | Description |
|------|------|----------|-------------|
| add_to_queue | Boolean | No | If `true`, automatically schedules the post to your next available queue slot. Cannot be used together with `scheduled_date`. |
| max_posts_per_slot | Integer | No | Override the profile's `max_posts_per_slot` setting for this request. Only used when `add_to_queue=true`. |

### Example Request

```bash
curl -X POST "https://api.upload-post.com/api/upload" \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -F "user=my_profile" \
  -F "platform[]=instagram" \
  -F "platform[]=tiktok" \
  -F "video=@my_video.mp4" \
  -F "title=My awesome video" \
  -F "add_to_queue=true"
```

### Example with Multiple Posts Per Slot

```bash
# First post goes to 9am slot
curl -X POST "https://api.upload-post.com/api/upload" \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -F "user=my_profile" \
  -F "platform[]=instagram" \
  -F "video=@video1.mp4" \
  -F "title=Instagram video" \
  -F "add_to_queue=true" \
  -F "max_posts_per_slot=3"

# Second post also goes to 9am slot (same slot, different platform)
curl -X POST "https://api.upload-post.com/api/upload" \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -F "user=my_profile" \
  -F "platform[]=facebook" \
  -F "video=@video2.mp4" \
  -F "title=Facebook video" \
  -F "add_to_queue=true" \
  -F "max_posts_per_slot=3"
```

### Success Response `202 Accepted`

```json
{
  "success": true,
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "scheduled_date": "2025-01-30T14:00:00+00:00",
  "queue_slot": "2025-01-30T14:00:00+00:00",
  "message": "Post added to queue"
}
```

---

## Get Queue Settings

Retrieve the current queue configuration for a profile.

| | |
|---|---|
| **Endpoint** | `GET /api/uploadposts/queue/settings` |
| **Authentication** | **Required**. `Authorization: Apikey <token>` |

### Query Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| profile_username | String | Yes | The profile to get settings for |

### Success Response `200 OK`

```json
{
  "success": true,
  "queue_settings": {
    "timezone": "America/New_York",
    "slots": [
      { "hour": 9, "minute": 0 },
      { "hour": 12, "minute": 0 },
      { "hour": 17, "minute": 0 }
    ],
    "days_of_week": [0, 1, 2, 3, 4, 5, 6],
    "max_posts_per_slot": 1,
    "full_slots": []
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| timezone | String | IANA timezone for the queue slots (e.g., "America/New_York", "Europe/Madrid") |
| slots | Array | Array of time slots with `hour` (0-23) and `minute` (0-59) |
| days_of_week | Array | Active days: 0=Monday, 1=Tuesday, ..., 6=Sunday |
| max_posts_per_slot | Integer | Maximum number of posts allowed per time slot (default: 1) |
| full_slots | Array | List of ISO 8601 datetimes that have been manually marked as full |

---

## Update Queue Settings

Update the queue configuration for a profile.

| | |
|---|---|
| **Endpoint** | `POST /api/uploadposts/queue/settings` |
| **Authentication** | **Required**. `Authorization: Apikey <token>` |

### Body Parameters (JSON)

| Name | Type | Required | Description |
|------|------|----------|-------------|
| profile_username | String | Yes | The profile to update settings for |
| timezone | String | No | IANA timezone (e.g., "Europe/London"). See [valid timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). |
| slots | Array | No | Array of slot objects: `[{ "hour": 9, "minute": 0 }, ...]`. Max 24 slots. |
| days_of_week | Array | No | Array of active days (0-6). Example: `[0, 1, 2, 3, 4]` for Monday-Friday. |
| max_posts_per_slot | Integer | No | Maximum posts per slot (1-100). Default: 1. Set higher to allow multiple posts in the same time slot. |

### Example Request

```bash
curl -X POST "https://api.upload-post.com/api/uploadposts/queue/settings" \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_username": "my_profile",
    "timezone": "Europe/Madrid",
    "slots": [
      { "hour": 8, "minute": 30 },
      { "hour": 13, "minute": 0 },
      { "hour": 19, "minute": 30 }
    ],
    "days_of_week": [0, 1, 2, 3, 4],
    "max_posts_per_slot": 3
  }'
```

### Success Response `200 OK`

```json
{
  "success": true,
  "queue_settings": {
    "timezone": "Europe/Madrid",
    "slots": [
      { "hour": 8, "minute": 30 },
      { "hour": 13, "minute": 0 },
      { "hour": 19, "minute": 30 }
    ],
    "days_of_week": [0, 1, 2, 3, 4],
    "max_posts_per_slot": 3
  }
}
```

---

## Get Queue Preview

Preview the next upcoming queue slots and their availability.

| | |
|---|---|
| **Endpoint** | `GET /api/uploadposts/queue/preview` |
| **Authentication** | **Required**. `Authorization: Apikey <token>` |

### Query Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| profile_username | String | Yes | The profile to preview |
| count | Integer | No | Number of slots to return (default: 10, max: 50) |

### Success Response `200 OK`

```json
{
  "success": true,
  "timezone": "America/New_York",
  "max_posts_per_slot": 3,
  "slots": [
    {
      "datetime_utc": "2025-01-30T14:00:00+00:00",
      "datetime_local": "2025-01-30T09:00:00-05:00",
      "available": true,
      "post_count": 0,
      "max_posts_per_slot": 3,
      "is_full": false,
      "manually_full": false
    },
    {
      "datetime_utc": "2025-01-30T17:00:00+00:00",
      "datetime_local": "2025-01-30T12:00:00-05:00",
      "available": true,
      "post_count": 2,
      "max_posts_per_slot": 3,
      "is_full": false,
      "manually_full": false,
      "scheduled_posts": [
        {
          "job_id": "abc123",
          "title": "My Instagram post",
          "platforms": ["instagram"]
        },
        {
          "job_id": "def456",
          "title": "My Facebook post",
          "platforms": ["facebook"]
        }
      ]
    },
    {
      "datetime_utc": "2025-01-30T22:00:00+00:00",
      "datetime_local": "2025-01-30T17:00:00-05:00",
      "available": false,
      "post_count": 3,
      "max_posts_per_slot": 3,
      "is_full": true,
      "manually_full": false,
      "scheduled_posts": [...]
    }
  ],
  "next_available": "2025-01-30T14:00:00+00:00"
}
```

| Field | Type | Description |
|-------|------|-------------|
| post_count | Integer | Number of posts currently scheduled in this slot |
| max_posts_per_slot | Integer | Maximum posts allowed per slot |
| is_full | Boolean | `true` if the slot is at capacity or manually marked as full |
| manually_full | Boolean | `true` if the slot was manually marked as full via the Mark Slot Full endpoint |
| scheduled_posts | Array | List of all posts scheduled in this slot (when multiple posts per slot is enabled) |
| scheduled_post | Object | First scheduled post in the slot (for backward compatibility) |

---

## Mark Slot Full

Manually mark a specific queue slot as full, preventing new posts from being added to it even if it hasn't reached `max_posts_per_slot`.

| | |
|---|---|
| **Endpoint** | `POST /api/uploadposts/queue/slot-full` |
| **Authentication** | **Required**. `Authorization: Apikey <token>` |

### Body Parameters (JSON)

| Name | Type | Required | Description |
|------|------|----------|-------------|
| profile_username | String | Yes | The profile to update |
| slot_datetime | String | Yes | ISO 8601 datetime of the slot to mark as full (UTC) |

### Example Request

```bash
curl -X POST "https://api.upload-post.com/api/uploadposts/queue/slot-full" \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_username": "my_profile",
    "slot_datetime": "2025-01-30T14:00:00+00:00"
  }'
```

### Success Response `200 OK`

```json
{
  "success": true,
  "message": "Slot 2025-01-30T14:00:00+00:00 marked as full",
  "full_slots": ["2025-01-30T14:00:00+00:00"]
}
```

---

## Unmark Slot Full

Remove the full mark from a slot, allowing new posts to be added again.

| | |
|---|---|
| **Endpoint** | `DELETE /api/uploadposts/queue/slot-full` |
| **Authentication** | **Required**. `Authorization: Apikey <token>` |

### Body Parameters (JSON)

| Name | Type | Required | Description |
|------|------|----------|-------------|
| profile_username | String | Yes | The profile to update |
| slot_datetime | String | Yes | ISO 8601 datetime of the slot to unmark (UTC) |

### Example Request

```bash
curl -X DELETE "https://api.upload-post.com/api/uploadposts/queue/slot-full" \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_username": "my_profile",
    "slot_datetime": "2025-01-30T14:00:00+00:00"
  }'
```

### Success Response `200 OK`

```json
{
  "success": true,
  "message": "Slot 2025-01-30T14:00:00+00:00 unmarked as full",
  "full_slots": []
}
```

---

## Get Next Available Slot

Get the next available queue slot for a profile.

| | |
|---|---|
| **Endpoint** | `GET /api/uploadposts/queue/next-slot` |
| **Authentication** | **Required**. `Authorization: Apikey <token>` |

### Query Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| profile_username | String | Yes | The profile to check |

### Success Response `200 OK`

```json
{
  "success": true,
  "next_slot": {
    "datetime_utc": "2025-01-30T14:00:00+00:00",
    "datetime_local": "2025-01-30T09:00:00-05:00",
    "timezone": "America/New_York"
  }
}
```

If no slots are available within the next 30 days:

```json
{
  "success": true,
  "next_slot": null,
  "message": "No available slots found"
}
```

---

## Default Configuration

If you haven't customized your queue settings, these defaults apply:

| Setting | Default Value |
|---------|---------------|
| Timezone | America/New_York (Eastern Time) |
| Slots | 9:00 AM, 12:00 PM, 5:00 PM |
| Days of week | All days (Monday-Sunday) |
| Max posts per slot | 1 |

---

## See Also

- [Upload Video](./upload-video.md) - Video upload endpoint
- [Upload Photos](./upload-photo.md) - Photo upload endpoint
- [Upload Text](./upload-text.md) - Text post endpoint
- [Schedule Posts](./schedule-posts.md) - Manage scheduled posts
- [Upload Status](./upload-status.md) - Check upload/job status


---
# API Reference
URL: https://docs.upload-post.com/api/reference

# API Reference

The Upload-Post API provides comprehensive endpoints for content management across multiple social media platforms. All endpoints require authentication via API key in the `Authorization` header.

## Core Upload APIs

### [Video Upload API](./upload-video.md)

Upload videos to TikTok, Instagram, LinkedIn, YouTube, Facebook, X (Twitter), Threads, Pinterest, Reddit, Bluesky, Discord, Telegram, and Google Business Profile. Supports both synchronous and asynchronous uploads with scheduling capabilities.

**Endpoint:** `POST /api/upload`

**Supported Platforms:** TikTok, Instagram, LinkedIn, YouTube, Facebook, X (Twitter), Threads, Pinterest, Reddit, Bluesky, Discord, Telegram, Google Business Profile, Mastodon, WordPress

### [Photo Upload API](./upload-photo.md)

Upload photos and image carousels to LinkedIn, Facebook, X (Twitter), Instagram, TikTok, Threads, Pinterest, Reddit, Bluesky, Discord, Telegram, and Google Business Profile. Perfect for visual content distribution across platforms.

**Endpoint:** `POST /api/upload_photos`

**Supported Platforms:** LinkedIn, Facebook, X (Twitter), Instagram, TikTok, Threads, Pinterest, Reddit, Bluesky, Discord, Telegram, Google Business Profile, Mastodon, Lemmy, WordPress

### [Text Upload API](./upload-text.md)

Create and distribute text-only posts across social platforms. Ideal for announcements, updates, and text-based content.

**Endpoint:** `POST /api/upload_text`

**Supported Platforms:** X (Twitter), LinkedIn, Facebook, Threads, Reddit, Bluesky, Discord, Telegram, Google Business Profile, Slack, Mastodon, Nostr, Lemmy, Dev.to, Hashnode, WordPress, Whop, Listmonk

### Platform How-To Guides

Step-by-step guides for the most requested platforms, with cURL, Python and JavaScript examples:

- [Post to TikTok via API](../guides/post-to-tiktok-api.md) — no TikTok developer app or audit required
- [Post to Instagram via API](../guides/post-to-instagram-api.md) — without your own Meta app approval
- [Post to YouTube via API](../guides/post-to-youtube-api.md) — no Google Cloud quota setup
- [Post to LinkedIn via API](../guides/post-to-linkedin-api.md) — profiles, company pages and documents
- [Post to X (Twitter) via API](../guides/post-to-x-twitter-api.md) — tweets, threads and media

## Upload Management APIs

### [Upload Status](./upload-status.md)

Track the progress and results of asynchronous uploads initiated with `async_upload=true` or scheduled posts. Essential for monitoring long-running upload operations and checking scheduled post execution status.

**Endpoint:** `GET /api/uploadposts/status`

**Parameters:** `request_id` (for async uploads) or `job_id` (for scheduled posts)

**Use Case:** Check status of background uploads and scheduled posts, get detailed results per platform

### [Upload History](./upload-history.md)

Retrieve a paginated history of all your past uploads across platforms. Includes detailed metadata, success/failure status, and platform-specific information.

**Endpoint:** `GET /api/uploadposts/history`

**Features:** Pagination, filtering, comprehensive upload metadata

### [Schedule Management](./schedule-posts.md)

Schedule posts for future publication across supported platforms. Manage your content calendar programmatically.

**Endpoint:** Various scheduling endpoints

**Supported Platforms:** X (Twitter), LinkedIn, Facebook, Instagram, TikTok, Bluesky, Threads, Pinterest, YouTube, Discord, Telegram

## Instagram Interactions

### [Media List](./instagram-media.md)

Retrieve a list of recent media (posts, reels, videos, pins, tweets, etc.) from any connected social media account. Supports Instagram, TikTok, YouTube, LinkedIn, Facebook, X, Threads, Pinterest, Bluesky, and Reddit.

**Endpoint:** `GET /api/uploadposts/media`

**Returns:** Media IDs, captions, media types, permalinks, timestamps, thumbnail URLs

**Pagination:** `limit` (default 25, max 100, capped per platform) and `cursor` for cursor-based paging. LinkedIn, Discord and Telegram support `limit` only.

### [Instagram Comments](./instagram-comments.md)

Retrieve comments on Instagram posts and send private replies (DMs) to commenters. Supports both media IDs and post URLs.

**Endpoints:**

- `GET /api/uploadposts/comments` - Get comments on an Instagram post
- `POST /api/uploadposts/comments/reply` - Send a private reply DM to a commenter

**Required Permission:** `instagram_business_manage_comments`

### [Instagram Direct Messages](./instagram-dms.md)

Send direct messages to Instagram users and retrieve DM conversations. Supports customer support workflows and follow-up messaging within Instagram's 24-hour messaging window policy.

**Endpoints:**

- `POST /api/uploadposts/dms/send` - Send a DM to an Instagram user by IGSID
- `GET /api/uploadposts/dms/conversations` - Retrieve Instagram DM conversations

**Required Permission:** `instagram_business_manage_messages`

### [AutoDM Monitors](./autodms.md)

Set up persistent monitors that automatically send private DMs to users who comment on your Instagram posts. Monitors run in the background 24/7 with built-in duplicate prevention and rate limiting.

**Endpoints:**

- `POST /api/uploadposts/autodms/start` - Start a new comment monitor
- `GET /api/uploadposts/autodms/status` - Get status of all monitors (supports `?include_inactive=true` to also return stopped/expired ones)
- `GET /api/uploadposts/autodms/logs` - Get activity logs for a monitor
- `POST /api/uploadposts/autodms/pause` - Pause a monitor
- `POST /api/uploadposts/autodms/resume` - Resume a paused monitor
- `POST /api/uploadposts/autodms/stop` - Stop a monitor
- `POST /api/uploadposts/autodms/delete` - Delete a monitor

**Limits:** 2 monitors per profile per day, auto-expires after 15 days

## Platform Integration APIs

### [Analytics API](./get-analytics.md)

Retrieve detailed analytics and performance metrics for your social media profiles across connected platforms.

**Endpoint:** `GET /api/analytics/{profile_username}`

**Supported Platforms:** Instagram, TikTok, LinkedIn, Facebook, X (Twitter), YouTube, Threads, Pinterest, Reddit

**Metrics:** Followers, impressions, reach, profile views, time-series data, metric_type, Instagram `follower_demographics` and `engaged_audience_demographics`

### [Total Impressions & Post Analytics](./get-analytics.md)

Get unified total impressions aggregated across platforms, per-post analytics, and platform metrics config. All analytics endpoints are consolidated in the Analytics API page.

**Endpoints:** `GET /api/uploadposts/total-impressions/{profile_username}` · `GET /api/uploadposts/post-analytics/{request_id}` · `GET /api/uploadposts/post-analytics?platform_post_id=` · `GET /api/uploadposts/post-analytics/cached`

**Features:** Date range filtering, per-platform breakdown, live post metrics, profile snapshots, analytics for organic posts via platform_post_id

### [Cached Post Analytics](./get-analytics.md#get-apiuploadpostspost-analyticscached)

Bulk replay of per-post metrics Upload-Post already fetched, instead of calling the platforms again. Only contains posts previously read through a live endpoint; there is no background refresh. Not subject to the 100 requests / 5 minutes platform analytics rate limit.

**Endpoint:** `GET /api/uploadposts/post-analytics/cached`

**Features:** Cursor pagination (up to 200 posts per page), `platform` filter, `since`/`until` date range, per-row `captured_at` freshness marker

### [Get Facebook Pages](./get-facebook-pages.md)

Retrieve all Facebook pages accessible through connected accounts. Required for posting to specific Facebook pages.

**Endpoint:** `GET /api/uploadposts/facebook/pages`

**Returns:** Page IDs, names, profile pictures, account associations

### [Get LinkedIn Pages](./get-linkedin-pages.md)

Fetch LinkedIn company pages associated with your connected accounts. Essential for business page posting.

**Endpoint:** `GET /api/uploadposts/linkedin/pages`

**Returns:** Organization URNs, company names, vanity URLs, page logos

### [Get Google Business Locations](./get-google-business-locations.md)

List the Google Business Profile locations available to an account. Essential for agencies and SaaS platforms where users manage multiple locations. There is no separate selection step: pass the chosen location's `name` as `gbp_location_id` on the upload request.

**Endpoint:** `GET /api/uploadposts/google-business/locations`

**Returns:** Location IDs and business names

**Post destinations:** Local Posts (the "Updates" tab, default) or the location's [Media/Gallery tab](./upload-photo.md#publishing-to-the-mediagallery-tab) via `gbp_post_type=MEDIA` + `gbp_media_category`

### [Get Pinterest Boards](./get-pinterest-boards.md)

List all Pinterest boards (public and secret) from connected accounts. Required for targeting specific boards when pinning content.

**Endpoint:** `GET /api/uploadposts/pinterest/boards`

**Returns:** Board IDs, names, associated Pinterest accounts

## User Management APIs

### [User Profiles API](./user-profiles.md)

Manage user profiles and generate JWTs for linking social accounts when integrating Upload-Post into your own platform. Essential for white-label integrations and multi-user applications.

**Endpoints:**

- `POST /api/uploadposts/users` - Create user profiles
- `GET /api/uploadposts/users` - Retrieve user profiles
- `DELETE /api/uploadposts/users` - Delete user profiles
- `POST /api/uploadposts/users/generate-jwt` - Generate authentication tokens
- `GET /api/uploadposts/users/validate-jwt` - Validate tokens

**White-label:** the connect page supports `en`, `es`, `de`, `fr`, `pt`, `pl` and `tr`, plus per-key text overrides via [`ui_labels`](./user-profiles.md#custom-ui-labels).

See the [User Profile Integration Guide](../guides/user-profile-integration.md) for implementation workflow.

### [Connect API](./connect-api.md)

Build your own social account connection page instead of using the hosted one. Server-side OAuth start endpoints return the authorize URL for each platform; a single-use state authenticates the callback, and the end user returns to your `redirect_url`.

**Endpoint:**

- `POST /api/uploadposts/oauth/{platform}/start` - Get the authorize URL for a platform

**Supported Platforms:** TikTok, Instagram, Facebook, LinkedIn, YouTube, X (Twitter), Threads, Reddit, Pinterest, Google Business Profile, Snapchat

### [Current User API](./current-user.md)

Validate your API key and retrieve basic account information including email and subscription plan.

**Endpoint:** `GET /api/uploadposts/me`

**Use Case:** API key validation, plan verification, account confirmation

## Content Requirements

### [Photo Requirements](./photo-requirements.md)

Comprehensive format specifications, file size limits, aspect ratios, and technical requirements for photo uploads across all supported platforms.

**Covers:** Instagram, TikTok, Facebook, X (Twitter), LinkedIn, Threads, Pinterest, Reddit, Bluesky, Discord, Telegram

### [Video Requirements](./video-requirements.md)

Detailed video format requirements, codec specifications, resolution limits, and encoding guidelines for optimal compatibility across platforms.

**Covers:** TikTok, Instagram, YouTube, LinkedIn, Facebook, X (Twitter), Threads, Pinterest, Reddit, Bluesky, Discord, Telegram

**Includes:** FFmpeg re-encoding solutions for compatibility issues

## MCP Server (AI Agents)

### [MCP Server Integration](../guides/mcp-server-integration.md)

Every endpoint on this page is also exposed through Upload-Post's official, open-source **Model Context Protocol** server. Point ChatGPT, claude.ai, Claude Desktop, Claude Code or Cursor at it and your agent can publish, schedule and analyze content without a hand-written REST client.

**Hosted endpoint:** `https://mcp.upload-post.com/mcp`

**Authentication:** API key, or OAuth 2.1 with PKCE for agents that authorize themselves

**Source:** [github.com/Upload-Post/upload-post-mcp](https://github.com/Upload-Post/upload-post-mcp) (MIT)

## Getting Started

1. **Authentication:** All requests require an API key in the `Authorization: Apikey your-api-key-here` header
2. **Base URL:** `https://api.upload-post.com/api`
3. **Rate Limits:** Free tier includes 10 uploads per month
4. **Content Guidelines:** Review platform-specific requirements before uploading

For implementation examples and integration guides, see our [SDK Examples](../sdk-examples.md) and [Integration Guides](../guides/user-profile-integration.md).


---
# Manage Scheduled Posts
URL: https://docs.upload-post.com/api/schedule-posts

# Manage Scheduled Posts

Schedule your uploads in advance and keep full control over them with our job management endpoints. This page covers how to list and cancel scheduled jobs created via the `scheduled_date` parameter.

---

## List Scheduled Posts

| |  |
|---|---|
| **Endpoint** | `GET /api/uploadposts/schedule` |
| **Authentication** | **Required**. Supply the Apikey in the `Authorization` header &mdash; e.g. `Authorization: Apikey <token>` |
| **Query / Body Params** | *None*. The user is inferred from the access-token. |

### Success Response `200 OK`

Returns a JSON array where each element is a scheduled-job object:

```json
[
  {
    "job_id": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
    "scheduled_date": "2024-12-25T10:30:00Z",
    "post_type": "video",
    "profile_username": "my_upload_post_profile",
    "title": "Merry Christmas!",
    "preview_url": "https://storage.googleapis.com/signed-url/video_preview.mp4?signature=..."
  }
]
```

| Field | Type | Description |
|-------|------|-------------|
| `job_id` | `string` | Unique identifier of the scheduled job. Required to cancel it. |
| `scheduled_date` | `string` | ISO-8601 date/time when the post will go live. **Time is in UTC**. |
| `post_type` | `string` | One of `video`, `photo`, or `text`. |
| `profile_username` | `string` | Upload-Post profile that will publish the content. |
| `title` | `string` | Title/caption of the post. |
| `preview_url` | `string \| null` | Short-lived signed URL to preview the media (first photo or video). `null` for text posts. |

#### Error Responses

| Status | Reason |
|--------|--------|
| `401 Unauthorized` | Missing or invalid token. |

---

## Cancel a Scheduled Post

| | |
|---|---|
| **Endpoint** | `DELETE /api/uploadposts/schedule/<job_id>` |
| **Authentication** | **Required**. Either an Apikey (`Authorization: Apikey <token>`) or a white-label profile JWT (`Authorization: Bearer <profile_jwt>`). When authenticated with a profile JWT, the job must belong to that profile and the profile must have `readonly_calendar: false`. |
| **URL Param** | `job_id` &mdash; ID obtained from the list endpoint. |

### Success Response `200 OK`

```json
{
  "success": true,
  "message": "Job <job_id> cancelled and assets deleted."
}
```

### Error Responses

| Status | Body | Condition |
|--------|------|-----------|
| `401 Unauthorized` | &nbsp; | Invalid or missing token. |
| `404 Not Found` | `{ "success": false, "error": "Job not found" }` | The supplied `job_id` does not exist or doesn't belong to the authenticated user. |
| `500 Internal Server Error` | &nbsp; | Unexpected failure while cancelling the job or deleting its assets. |

---

## Edit a Scheduled Post

| | |
|---|---|
| **Endpoint** | `PATCH /api/uploadposts/schedule/<job_id>` |
| **Authentication** | **Required**. Either an Apikey (`Authorization: Apikey <token>`) or a white-label profile JWT (`Authorization: Bearer <profile_jwt>`). When authenticated with a profile JWT, the job must belong to that profile and the profile must have `readonly_calendar: false`. |
| **URL Param** | `job_id` &mdash; ID obtained from the list endpoint. |
| **Body** | JSON object with one or more of the fields below. |

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `scheduled_date` | `string` | No | ISO-8601 date/time, e.g., `"2025-10-05T10:30:00Z"`. Must be in the future and within 1 year. Interpreted as UTC unless `timezone` is provided. |
| `timezone` | `string` | No | IANA timezone identifier (e.g., `"Europe/Madrid"`, `"America/New_York"`). If provided, `scheduled_date` is interpreted in this timezone. Defaults to UTC if omitted. See [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). |
| `title` | `string` | No | New post title/caption. |
| `caption` | `string` | No | New caption/description. |

### Success Response `200 OK`

```json
{
  "success": true,
  "job_id": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
  "scheduled_date": "2025-10-05T10:30:00Z",
  "title": "Updated title",
  "caption": "Updated caption"
}
```

### Error Responses

| Status | Body | Condition |
|--------|------|-----------|
| `400 Bad Request` | `{ "success": false, "error": "<reason>" }` | Invalid body; invalid/past date; job not editable; daily limit reached. |
| `401 Unauthorized` | &nbsp; | Invalid or missing token. |
| `403 Forbidden` | `{ "success": false, "error": "Forbidden" }` | The job does not belong to the authenticated user, or the profile JWT does not match the job's profile. |
| `403 Forbidden` | `{ "success": false, "message": "This calendar is read-only…", "error_code": "READONLY_CALENDAR" }` | Authenticated with a profile JWT for a profile that has `readonly_calendar: true`. |
| `404 Not Found` | `{ "success": false, "error": "Job not found" }` | The supplied `job_id` does not exist. |
| `500 Internal Server Error` | &nbsp; | Unexpected failure while editing the job. |

#### Example Request

```bash
curl -X PATCH "https://api.upload-post.com/api/uploadposts/schedule/JOB_ID" \
  -H "Content-Type: application/json" \
  -H "Authorization: Apikey <token>" \
  -d '{
    "scheduled_date": "2025-10-05T10:30:00",
    "timezone": "Europe/Madrid",
    "title": "Updated title",
    "caption": "Updated caption"
  }'
```

---

## See Also

* [Using `scheduled_date` when uploading content](./upload-video.md#common-parameters) – parameter description.
* [Upload Video](./upload-video.md), [Upload Photos](./upload-photo.md), [Upload Text](./upload-text.md) – endpoints that support scheduling.
* [Upload Status](./upload-status.md) – Check the execution status of scheduled posts using the `job_id`.


---
# Upload Document
URL: https://docs.upload-post.com/api/upload-document

# Upload Document

Upload documents (PDF, PPT, PPTX, DOC, DOCX) to LinkedIn as native document posts. Documents are displayed as carousels/viewers on LinkedIn.

### Endpoint

```http
POST /api/upload_document
```

### Headers

| Name | Value | Description |
|------|-------|-------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

### Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| user | String | Yes | User identifier (profile name) |
| platform[] | Array | Yes | Must be `["linkedin"]` - only LinkedIn supports document uploads |
| document | File/URL | Yes | The document file to upload (file upload or URL). Supported formats: PDF, PPT, PPTX, DOC, DOCX |
| title | String | Yes | Document title (displayed on the post) |
| description | String | No | Post commentary/text that appears above the document |
| visibility | String | No | Visibility setting: "PUBLIC", "CONNECTIONS", "LOGGED_IN", or "CONTAINER". Default: "PUBLIC" |
| target_linkedin_page_id | String | No | LinkedIn organization/page ID to post to a company page instead of personal profile |
| first_comment | String | No | Automatically post a first comment after publishing the document. |
| linkedin_first_comment | String | No | Platform-specific first comment override. Takes priority over `first_comment`. |

### Document Requirements

| Requirement | Value |
|-------------|-------|
| Supported Formats | PDF, PPT, PPTX, DOC, DOCX |
| Maximum File Size | 100 MB |
| Maximum Pages | 300 pages |

### Example Request (File Upload)

```bash
curl -X POST "https://api.upload-post.com/api/upload_document" \
  -H "Authorization: Apikey your-api-key-here" \
  -F "user=your_profile" \
  -F "platform[]=linkedin" \
  -F "document=@/path/to/document.pdf" \
  -F "title=My Presentation" \
  -F "description=Check out this presentation on our latest product updates!"
```

### Example Request (URL)

```bash
curl -X POST "https://api.upload-post.com/api/upload_document" \
  -H "Authorization: Apikey your-api-key-here" \
  -F "user=your_profile" \
  -F "platform[]=linkedin" \
  -F "document=https://example.com/document.pdf" \
  -F "title=My Presentation" \
  -F "description=Check out this presentation!"
```

### Example Request (Company Page)

```bash
curl -X POST "https://api.upload-post.com/api/upload_document" \
  -H "Authorization: Apikey your-api-key-here" \
  -F "user=your_profile" \
  -F "platform[]=linkedin" \
  -F "document=@/path/to/document.pdf" \
  -F "title=Company Update Q4 2024" \
  -F "description=Our quarterly report is now available." \
  -F "target_linkedin_page_id=12345678" \
  -F "visibility=PUBLIC"
```

### Success Response

```json
{
  "success": true,
  "message": "Document uploaded successfully",
  "request_id": "abc123def456",
  "results": {
    "linkedin": {
      "success": true,
      "document_urn": "urn:li:document:1234567890",
      "post_id": "urn:li:activity:7654321098765432",
      "url": "https://www.linkedin.com/feed/update/urn:li:activity:7654321098765432/",
      "platform": "linkedin",
      "content_type": "document",
      "file_size": 1048576,
      "filename": "document.pdf"
    }
  }
}
```

### Error Response

```json
{
  "success": false,
  "message": "Document upload failed: Your LinkedIn session has expired. Please reconnect your LinkedIn account at app.upload-post.com/manage-users.",
  "request_id": "abc123def456",
  "results": {
    "linkedin": {
      "success": false,
      "error": "Your LinkedIn session has expired. Please reconnect your LinkedIn account at app.upload-post.com/manage-users."
    }
  }
}
```

### How Documents Appear on LinkedIn

When you upload a document:

1. **Native Viewer**: LinkedIn displays the document in a native carousel/viewer format
2. **Page Navigation**: Users can swipe or click through pages
3. **Preview**: LinkedIn generates thumbnail previews for each page
4. **Download**: Depending on visibility settings, users may be able to download the document

### Platform Limitations

| Platform | Document Support |
|----------|------------------|
| LinkedIn | Yes (native carousel/viewer) |
| Facebook | No |
| Instagram | No |
| TikTok | No |
| X (Twitter) | No |
| YouTube | No |
| Pinterest | No |
| Threads | No |
| Bluesky | No |
| Reddit | No |

### Notes

- Document processing may take a few seconds on LinkedIn's side before the post becomes fully visible
- The document title appears as the post's media title
- The description/commentary appears as the post text above the document
- For company pages, ensure the authenticated LinkedIn account has admin access to the page
- LinkedIn may compress or optimize documents for viewing

### Related Endpoints

- [Upload Video](./upload-video) - Upload videos to multiple platforms
- [Upload Photo](./upload-photo) - Upload images to multiple platforms
- [Upload Text](./upload-text) - Post text-only content
- [Get LinkedIn Pages](./get-linkedin-pages) - List available LinkedIn pages for your account


---
# Upload History
URL: https://docs.upload-post.com/api/upload-history

# Upload History

Retrieve a paginated list of your past uploads across platforms.

## Endpoint

```http
GET /api/uploadposts/history
```

## Headers

| Name | Value | Description |
|------|-------|-------------|
| Authorization | Apikey your-api-key | Required.|

## Query Parameters

| Name | Type | Required | Default | Allowed | Description |
|------|------|----------|---------|---------|-------------|
| page | Integer | No | 1 | >= 1 | Page number |
| limit | Integer | No | 10 | 10, 20, 50, 100 | Page size |

## Responses

- 200 OK
  - `history`: array of history items (most recent first)
  - `total`: total number of records for the user
  - `page`: requested page
  - `limit`: requested limit
- 400 Bad Request: `{ "error": "Invalid page" }` or `{ "error": "Invalid limit" }`
- 401 Unauthorized: `{ "success": false, "message": "Invalid or expired token" }`
- 500 Internal Server Error: `{ "error": "Failed to retrieve upload history", "details": "..." }`

## History Item Schema

Typical fields (not all fields are guaranteed on every record):

- `user_email`: string
- `profile_username`: string
- `platform`: string (e.g., `tiktok`, `instagram`, `linkedin`, `youtube`, `facebook`, `x`, `threads`, `pinterest`, `google_business`, `discord`, `telegram`)
- `media_type`: string (`video` | `photo` | `text`)
- `upload_timestamp`: string (ISO-8601)
- `success`: boolean
- `platform_post_id`: string | array | null
- `post_url`: string | null (present when `success` is true)
- `error_message`: string | null
- `media_size_bytes`: number | null
- `post_title`: string | null
- `post_caption`: string | null
- `is_async`: boolean | null
- `job_id`: string | null (present when the upload originated from a scheduled job)
- `dashboard`: any | null
- `video_was_transcoded`: boolean | null
- `changes`: object | null
- `prevalidation_metadata`: object | null
- `request_id`: string | null
- `request_total_platforms`: number | null

> Note: When you schedule a post, the resulting history items will include `job_id`. Use this to correlate the scheduled job with the eventual publish record in history.

## Example Request

```bash
curl -X GET "https://api.upload-post.com/api/uploadposts/history?page=1&limit=20" \
  -H "Authorization: Apikey your-api-key"
```

## Example 200 Response (truncated)

```json
{
  "history": [
    {
      "user_email": "user@example.com",
      "profile_username": "profile_username",
      "platform": "instagram",
      "media_type": "video",
      "upload_timestamp": "2025-09-04T10:22:33.123Z",
      "success": true,
      "platform_post_id": "1789654321",
      "post_url": "https://instagram.com/p/abc123",
      "media_size_bytes": 12345678,
      "post_title": "Title",
      "post_caption": "Description",
      "is_async": false,
      "job_id": "a1b2c3d4e5f67890a1b2c3d4e5f67890",
      "dashboard": true,
      "request_id": "req_123",
      "request_total_platforms": 3
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 20
}
```

## See also

- [Upload Status](./upload-status)
- [Manage Scheduled Posts](./schedule-posts)
- [Upload Text](./upload-text)
- [Upload Video](./upload-video)
- [Upload Photos](./upload-photo)


---
# Upload Photos
URL: https://docs.upload-post.com/api/upload-photo

# Upload Photos

Upload photos (and mixed media for supported platforms) to various social media platforms using this endpoint.

### Endpoint

```
POST /api/upload_photos
```

### Headers

| Name          | Value                      | Description                              |
|---------------|----------------------------|------------------------------------------|
| Authorization | Apikey your-api-key-here   | Your API key for authentication          |
| Idempotency-Key | unique-string | Optional. Prevents duplicate uploads if the same request is retried (e.g., after a timeout). Can also be sent as `X-Idempotency-Key` or `X-Request-Id`. When provided, if a matching upload job already exists, the API returns the existing job instead of creating a duplicate. |

### Common Parameters

| Name       | Type   | Required | Description                                                                                   |
|------------|--------|----------|-----------------------------------------------------------------------------------------------|
| user       | String | Yes      | User identifier                                                                               |
| platform[] | Array  | Yes      | Platform(s) to upload to. Supported values: tiktok, instagram, linkedin, facebook, x, threads, pinterest, bluesky, reddit, discord, telegram, google_business, mastodon, lemmy, wordpress |
| photos[]   | Array  | Yes      | Array of files to upload. Accepts **photos** (jpg, png, etc.). <br /> **Note:** You can also include **videos** (mp4, mov, etc.) ONLY for **Instagram** and **Threads** mixed carousels. |
| title      | String | Conditional | Default title/caption of the post. **Required** for Reddit. Optional for all other platforms (TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest). |
| description    | String | No       | Optional extended text used on TikTok photo descriptions, LinkedIn commentary, Facebook descriptions, Pinterest notes, and Reddit bodies. Ignored elsewhere. |
| request_id | String | No | Client-provided request identifier. If omitted, the server generates one. Returned in every response and used to track the upload via [Upload Status](./upload-status). Useful when `async_upload=true` and the HTTP response might be lost (e.g., timeout). Can also be sent as an `X-Request-Id` header. |
| scheduled_date | String (ISO-8601) | No | Optional date/time (ISO-8601) to schedule publishing, e.g., "2024-12-31T23:45:00Z". Must be in the future (≤ 365 days). Omit for immediate upload. |
| timezone | String (IANA) | No | Optional timezone identifier (e.g., "Europe/Madrid", "America/New_York"). If provided, `scheduled_date` is interpreted in this timezone. Defaults to UTC if omitted. See [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for valid values. |
| async_upload  | Boolean | No      | If `true`, the request returns immediately with a `request_id` and processes in the background. See [Upload Status](./upload-status). |
| add_to_queue  | Boolean | No      | If `true`, automatically schedules the post to your next available queue slot. Cannot be used with `scheduled_date`. See [Queue System](./queue-system). |
| max_posts_per_slot | Integer | No | Override the profile's max posts per slot setting for this request. Only used when `add_to_queue=true`. See [Queue System](./queue-system). |
| first_comment | String | No       | Automatically post a first comment after publishing. Supported on Instagram, Facebook, Threads, Bluesky, Reddit, X, YouTube, and LinkedIn. On X (Twitter) and Threads, this creates a reply to the main post. For X threads, the comment is posted as a reply to the last tweet in the thread. On YouTube, it posts as a top-level comment on the video. |
| first_comment_media[] | File(s) | No | Image files to attach to the first comment as inline images. Currently supported on Reddit. Not available for scheduled or queued posts. |

> **Important:** If you set `async_upload` to `false` but the upload takes longer than 59 seconds, it will automatically switch to asynchronous processing to avoid timeouts. In that case, use the `request_id` with the [Upload Status](./upload-status) endpoint to check the upload status and result.

### Platform-Specific First Comments

The `first_comment` parameter serves as a fallback. To set a custom first comment for a particular platform, use the optional `[platform]_first_comment` parameter. If provided, it will override the main `first_comment` for that platform.

**Example Optional Parameters:**

*   `instagram_first_comment`: "Follow for more content! #photography"
*   `facebook_first_comment`: "Let me know your thoughts in the comments!"
*   `x_first_comment`: "Thread incoming! 🧵"
*   `threads_first_comment`: "First comment on Threads!"
*   `reddit_first_comment`: "Source in the comments."
*   `bluesky_first_comment`: "More details in the replies."
*   `linkedin_first_comment`: "Source article in the comments."

> **Scheduling behavior:** When you provide `scheduled_date`, the API responds with `202 Accepted` and includes a `job_id`. That same `job_id` will later appear in [Upload History](./upload-history) to correlate the scheduled job with the publish record. You can also use the `job_id` with the [Upload Status](./upload-status) endpoint to check the execution status of the scheduled post.

> **Video Support (Mixed Carousels):**
> - **Instagram & Threads:** You can upload videos in the `photos[]` array to create **mixed carousels** (photos + videos).
> - **All other platforms (Facebook, TikTok, LinkedIn, X, Pinterest):** Do **NOT** upload videos to this endpoint. Use the [Upload Video](./upload-video) endpoint instead. Uploading videos here for these platforms will result in an error.

### Platform-Specific Titles

The `title` parameter serves as a fallback. To set a custom title for a particular platform, use the optional `[platform]_title` parameter. If provided, it will override the main `title` for that platform.

**Example Optional Parameters:**

*   `instagram_title`: "Check out my latest reel on Instagram! #reels"
*   `facebook_title`: "Excited to share this new video with my Facebook friends and family."
*   `tiktok_title`: "New TikTok video just dropped! 🔥"
*   `linkedin_title`: "A professional insight on the latest industry trends, discussed in this video."
*   `x_title`: "New video out now! 📢"

### Platform-Specific Parameters

### LinkedIn

| Name                    | Type   | Required | Description                                                    | Default     |
|-------------------------|--------|----------|----------------------------------------------------------------|-------------|
| linkedin_title          | String | No       | Specific title for the LinkedIn post. Fallbacks to `title`.    | `title`     |
| linkedin_description or description | String | No | Sent as the post commentary. If omitted, we reuse `title`. | `title`     |
| visibility              | String | No       | Visibility setting for the post (accepted value: "PUBLIC")     | PUBLIC      |
| target_linkedin_page_id | String | No       | LinkedIn page ID to upload photos to an organization         | "107579166" |

### Facebook

| Name             | Type   | Required | Description                                                       | Default |
|------------------|--------|----------|-------------------------------------------------------------------|---------|
| facebook_title   | String | No       | Specific title for the Facebook post. Fallbacks to `title`.       | `title` |
| facebook_page_id | String | Yes      | Facebook Page ID where the photos will be posted                  | -       |
| facebook_media_type | String | No | Type of media ("POSTS" or "STORIES") | "POSTS" |

Note: The caption is applied only to the first photo uploaded. For correct posting on Facebook, ensure the Page is directly associated with your personal profile and not managed through a Business Portfolio.

> Note: If `facebook_page_id` is not provided, we will automatically use the user's only connected Page (if exactly one exists). If multiple Pages are connected, the API returns a helpful error with an `available_pages` list so you can choose one. Posting to personal Facebook profiles via API is not supported by Meta; only Pages can be posted to.

:::info Select the target Page on each Facebook upload
Connecting Facebook only links your account; it does **not** pick a destination
Page. You must select the target **Page on every upload** by passing
`facebook_page_id`. When exactly one Page is connected it is auto-selected;
when multiple Pages are connected and no `facebook_page_id` is given, the API
returns an `available_pages` list so you can choose. Use
[Get Facebook Pages](./get-facebook-pages.md) to look up the `page_id` values.
:::

### X (Twitter)

:::warning URLs are stripped from every X post
Upload-Post removes every URL that X would turn into a clickable link from
the caption, title, and `first_comment` before sending the tweet — schemed
URLs (`https://`, `http://`, `ftp://`), `www.` hosts, shorteners (`t.co`,
`bit.ly`, …), bare hostnames with a path (`example.com/foo`), and IPs with a
path.

Obfuscated forms (`example[.]com`, `hxxp://`, unicode dots) are **not**
stripped because X does not parse them as links — they display as plain text
and are billed at the normal `$0.015` rate.

**Why:** X charges `$0.200` per post containing a URL vs `$0.015` without —
13× more. See the
[Character Limits page](../resources/character-limits.md#x-twitter-character-limits)
for details.
:::

| Name                        | Type    | Required | Description                                                                                                                           | Default     |
|-----------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------|-------------|
| x_title                      | String  | No       | Specific title for the tweet. Fallbacks to `title`.                                                                                   | `title`     |
| x_long_text_as_post          | Boolean | No       | When `true`, publishes long text as a single post. Otherwise, creates a thread.                                                      | `false`     |
| x_thread_image_layout        | String  | No       | Comma-separated list of how many images to attach to each tweet in the thread. Each value must be 0-4, and the total must equal the number of images. Use `0` for a text-only tweet in the thread (e.g., `"0,4"` makes the first tweet text-only and attaches all 4 images to the second tweet). Example: `"4,4"` puts 4 images in each of 2 tweets; `"2,3,1"` puts 2 in the first, 3 in the second, 1 in the third. If omitted and more than 4 images are provided, defaults to auto-chunking into groups of 4. | auto        |
| reply_settings               | String  | No       | Controls who can reply to the tweet ("following", "mentionedUsers", "subscribers", "verified")                                       | -           |
| geo_place_id                 | String  | No       | Place ID for adding geographic location to the tweet                                                                                  | -           |
| nullcast                     | Boolean | No       | Whether to publish without broadcasting (promotional/promoted-only posts)                                                             | `false`     |
| for_super_followers_only     | Boolean | No       | Tweet exclusive for super followers                                                                                                   | `false`     |
| community_id                 | String  | No       | Community ID for posting to specific communities                                                                                      | -           |
| share_with_followers         | Boolean | No       | Share community post with followers                                                                                                   | `false`     |
| direct_message_deep_link     | String  | No       | Link to take the conversation from public timeline to private Direct Message                                                         | -           |
| tagged_user_ids              | Array   | No       | Array of user IDs to tag in the photos (max 10 users)                                                                                 | []          |
| reply_to_id                  | String  | No       | ID of the tweet to reply to. Creates a reply to the specified tweet.                                                                  | -           |
| exclude_reply_user_ids       | Array   | No       | Array of user IDs to exclude from replying to this tweet. Requires `reply_to_id`.                                                    | []          |

Note: For Twitter uploads, specify the platform as "x" in the platform[] array.

:::warning Quote tweets cannot include media
Because photo uploads always attach media, `quote_tweet_id` cannot be used on
this endpoint. X treats media and `quote_tweet_id` as mutually exclusive, so a
photo post that also sets `quote_tweet_id` is rejected with:

> `quote_tweet_id cannot be used when uploading media to X. Remove it and retry.`

To quote a tweet, post it as a text-only quote tweet via [Upload Text](./upload-text.md)
and share the image as a separate tweet.
:::

:::info Replies can return 403 on the Pay-Per-Use tier
When you use `reply_to_id`, X may reject the reply with **HTTP 403** if the
connected account is on X's **Pay-Per-Use** tier and has **not engaged with the
author** being replied to (doesn't follow them / hasn't interacted with them).
This is an X platform restriction, not an auth problem — reconnecting will not
fix it. **Workaround:** reply to an author the account has already engaged with.
:::

> **More than 4 images:** X supports a maximum of 4 images per tweet. If you provide more than 4 images, the API will automatically create a thread, distributing images across multiple tweets (up to 4 images each). Use `x_thread_image_layout` to control exactly how images are distributed across tweets.

The global `description` field is ignored for X photo uploads.

#### How X (Twitter) Thread Creation Works (Advanced Logic)
**Note:** The following describes the default thread creation logic. To override this and post long text as a single post, set the `x_long_text_as_post` parameter to `true`.

The system is engineered to create well-formatted, natural-looking threads on X (formerly Twitter). Instead of simply splitting text at every line break, it intelligently groups paragraphs to create more readable tweets.

Here's the step-by-step logic:

**Intelligent Paragraph Grouping (Primary Method):**

The function first identifies distinct paragraphs (any text separated by a blank line).
It then combines as many of these paragraphs as possible into a single tweet, filling it up to the 280-character limit without exceeding it. The double newline (`\n\n`) between combined paragraphs is preserved for formatting.
This results in fewer, more substantial tweets that flow naturally, just as if a person had written them.

**Handling Exceptionally Long Paragraphs:**

If a single paragraph is, by itself, longer than the 280-character limit, a more granular splitting logic is automatically triggered for that paragraph only:

- **Split by Line Break:** The system first attempts to break the paragraph down by its individual line breaks (`\n`).
- **Split by Word:** If any of those single lines are still too long, it will split them by words as a final resort.

**Media Attachment:**

Images are distributed across tweets according to the `x_thread_image_layout` parameter. If not specified and more than 4 images are provided, they are automatically distributed in groups of 4. Text parts and image chunks are interleaved across the thread tweets.

### TikTok

| Name                  | Type    | Required | Description                                                                                 | Default |
|-----------------------|---------|----------|---------------------------------------------------------------------------------------------|---------|
| tiktok_title          | String  | No       | Specific title for the TikTok post (max 90 characters). Fallbacks to `title`.               | `title` |
| post_mode             | String  | No      | Controls how the upload is handled. `DIRECT_POST` publishes immediately; `MEDIA_UPLOAD` sends the media to the TikTok inbox so users can finish editing in-app. | DIRECT_POST       |
| privacy_level         | String  | No | Accepted values: `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY`. | PUBLIC_TO_EVERYONE |
| auto_add_music        | Boolean | No       | Automatically add background music to photos                                                | false   |
| disable_comment       | Boolean | No       | Disable comments on the post                                                                | false   |
| brand_content_toggle | Boolean| No       | Set to `true` for paid partnerships that promote third-party brands.        | false   |
| brand_organic_toggle | Boolean| No       | Set to `true` when promoting the creator's own business.                    | false   |
| photo_cover_index     | Integer | No       | Index (starting at 0) of the photo to use as the cover/thumbnail for the TikTok photo post  | 0       |
| tiktok_description or description    | String  | No       | For photo posts, used as description inside `post_info` (max 4,000 characters). | `title`   |

> **Note on Draft Mode (`MEDIA_UPLOAD`):** When using `MEDIA_UPLOAD` mode (Draft), TikTok does not allow setting a title, caption, privacy settings, or other metadata via the API. The video is simply uploaded to your TikTok inbox/drafts, and you must add the title, caption, and settings manually within the TikTok app before publishing.

### Instagram

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| instagram_title | String | No       | Specific title for the Instagram post. Fallbacks to `title`. | `title` |
| media_type | String | No | Type of media ("IMAGE" or "STORIES"). Automatically handles CAROUSEL/REELS logic if mixed media is detected. | "IMAGE" |
| collaborators | String | No | Comma-separated list of collaborator usernames. | - |
| user_tags | String | No | Users to tag on the photo. **Photo posts require x/y coordinates** — see below. | - |
| location_id | String | No | Instagram location ID. | - |

#### Note on Instagram `user_tags` for photo posts

Instagram's Graph API requires `x` and `y` coordinates (floats between `0.0` and `1.0`, marking the tag position on the image) whenever you tag a user on a **photo** post. Username-only tags are silently dropped by Instagram on photos — the post still publishes, but without the tag.

Send `user_tags` as a JSON-encoded array of objects:

```json
"user_tags": "[{\"username\":\"glassdojo\",\"x\":0.5,\"y\":0.5}]"
```

Tag multiple users by adding more objects, each with its own coordinates:

```json
"user_tags": "[{\"username\":\"user1\",\"x\":0.3,\"y\":0.4},{\"username\":\"user2\",\"x\":0.7,\"y\":0.6}]"
```

For carousels, the same tags are applied to every image in the carousel.

> **Reels/videos** accept the simpler comma-separated form (`"@user1, user2"`) because Instagram does not require coordinates for video tags. See [Upload Video](./upload-video.md#instagram).

The global `description` field is ignored for Instagram uploads (title serves as caption).

### Threads

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| threads_title | String | No | Specific title for the Threads post. Fallbacks to `title`. | `title` |
| threads_thread_media_layout | String | No | Comma-separated list of how many media items to include in each Threads post. Each value must be 0-10, and the total must equal the number of files. Use `0` for a text-only post in the thread (e.g., `"0,5"` makes the first post text-only and attaches all 5 media items to the second post). Example: `"5,5"` splits 10 items into 2 posts of 5 each; `"3,4,3"` splits 10 items into 3 posts. If omitted and more than 10 items are provided, defaults to auto-chunking into groups of 10. | auto |
| threads_topic_tag | String | No | A topic tag for the post (1-50 characters). Cannot contain periods (.) or ampersands (&). One tag per post. Helps increase reach. | - |

> **More than 10 items:** Threads supports a maximum of 10 media items per post (carousel). If you provide more than 10 items, the API will automatically create multiple posts, distributing media across posts (up to 10 items each). Use `threads_thread_media_layout` to control exactly how media items are distributed across posts.

The global `description` field is ignored for Threads photo uploads.

### Pinterest

| Name                 | Type   | Required | Description                                  | Default |
|----------------------|--------|----------|----------------------------------------------|---------|
| pinterest_title      | String | No       | Specific title for the Pinterest Pin. Fallbacks to `title`. | `title` |
| pinterest_description or description | String | No | Populates the Pin description. If omitted, we reuse `title`. | `title` |
| pinterest_board_id   | String | Yes      | Pinterest board ID to publish the photo to.  | -       |
| pinterest_alt_text   | String | No       | Alt text for the image.                      | -       |
| pinterest_link       | String | No       | Destination link for the photo Pin.          | -       |

### Bluesky

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| bluesky_title | String | No | Specific text for the Bluesky post. Fallbacks to `title`. | `title` |

Note: Bluesky supports up to 4 images per post.

### Reddit

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| reddit_title | String | No | Specific title for the Reddit post. Fallbacks to `title`. | `title` |
| subreddit | String | Yes | Name of the subreddit to post to (without "r/"). | - |
| flair_id | String | No | ID of the flair to apply to the post. | - |

Note: Reddit photo posts support a single image. The image will be uploaded as a native Reddit image post.

### Discord

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| discord_title | String | No | Caption sent alongside the image(s). Fallbacks to `title`. | `title` |

Note: Discord posts the images as message attachments to the channel behind the connected webhook (see [Connecting Discord](../guides/connecting-accounts.md#connecting-discord-manual-credentials)). Up to **10 images** are supported per message; the optional caption is limited to 2000 characters.

### Telegram

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| telegram_title | String | No | Caption sent alongside the image(s). Fallbacks to `title`. | `title` |

Note: Your connected bot delivers the image(s) to the configured chat/channel (see [Connecting Telegram](../guides/connecting-accounts.md#connecting-telegram-manual-credentials)). A single photo is sent via `sendPhoto`; multiple photos are sent as an album via `sendMediaGroup`. The optional caption is limited to **1024 characters**.

### Mastodon

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| mastodon_title | String | No | Caption for the Mastodon post. Fallbacks to `title`. | `title` |

Note: Each image is uploaded to your instance and attached to the status (see [Connecting Mastodon](../guides/connecting-accounts.md#connecting-mastodon-manual-credentials)). Up to **4 images** per post.

### Lemmy

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| lemmy_title | String | No | Title of the Lemmy post (required by Lemmy). Fallbacks to `title`. | `title` |

Note: The first image is uploaded to the instance's image host and set as the post URL (see [Connecting Lemmy](../guides/connecting-accounts.md#connecting-lemmy-manual-credentials)). A single image per post; Lemmy posts always require a title.

### WordPress

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| wordpress_title | String | No | Post title. Fallbacks to `title`. | `title` |

Note: Images are uploaded to the WordPress media library; the first becomes the post's featured image (see [Connecting WordPress](../guides/connecting-accounts.md#connecting-wordpress-manual-credentials)).

### Google Business Profile

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| gbp_location_id | String | No* | The location to post to. Use [Get Google Business Locations](./get-google-business-locations.md) to list available locations. | Auto |
| gbp_topic_type | String | No | Post type: `STANDARD` (default), `EVENT`, or `OFFER`. | `STANDARD` |
| gbp_cta_type | String | No | Call-to-action button: `BOOK`, `ORDER`, `SHOP`, `LEARN_MORE`, `SIGN_UP`, `CALL`. | - |
| gbp_cta_url | String | Conditional | URL for the CTA button. Required if `gbp_cta_type` is set. | - |
| gbp_post_type | String | No | Set to `MEDIA` (also accepts `PHOTO` or `GALLERY`) to publish the photo to the location's **Media/Gallery** tab instead of creating a Local Post. See [Publishing to the Media/Gallery tab](#publishing-to-the-mediagallery-tab). | - |
| gbp_upload_to_gallery | Boolean | No | Alternative to `gbp_post_type`. Set to `true` for the same Media/Gallery behaviour. | `false` |
| gbp_media_category | String | No | Category assigned to the uploaded photo in the Media tab. Alias: `media_category`. Only used when publishing to the Media/Gallery tab. | `ADDITIONAL` |

> Note: If `gbp_location_id` is not provided, the API will automatically use the account's only location (if exactly one exists). If multiple locations are connected, the API returns an error asking you to select one.

**Event parameters** (when `gbp_topic_type` is `EVENT`):

| Name | Type | Required | Description |
|------|------|----------|-------------|
| gbp_event_title | String | Yes | Title of the event. |
| gbp_event_start_date | String | Yes | Start date in `YYYY-MM-DD` format. |
| gbp_event_start_time | String | No | Start time in `HH:MM` format (24h). |
| gbp_event_end_date | String | Yes | End date in `YYYY-MM-DD` format. |
| gbp_event_end_time | String | No | End time in `HH:MM` format (24h). |

**Offer parameters** (when `gbp_topic_type` is `OFFER`):

| Name | Type | Required | Description |
|------|------|----------|-------------|
| gbp_coupon_code | String | No | Coupon or promo code. |
| gbp_redeem_url | String | No | URL where the offer can be redeemed. |
| gbp_terms | String | No | Terms and conditions of the offer. |

#### Publishing to the Media/Gallery tab

By default, publishing to Google Business creates a **Local Post** — an entry in the location's "Updates" tab. You can instead push a photo straight to the location's **Media/Gallery** tab (the photo gallery customers see on the business listing).

To do so, send one of these fields on the upload request:

| Name | Type | Description |
|------|------|-------------|
| gbp_post_type | String | `MEDIA`, `PHOTO` or `GALLERY` — all three select the Media/Gallery tab. |
| gbp_upload_to_gallery | Boolean | `true` — equivalent to `gbp_post_type=MEDIA`. |

> **Backwards compatible:** if `gbp_post_type` is omitted, or is set to anything other than `MEDIA`/`PHOTO`/`GALLERY`, the request keeps the existing Local Post behaviour, unchanged.

**Media categories**

`gbp_media_category` (alias: `media_category`) sets the category the photo is filed under in the Media tab. It defaults to `ADDITIONAL`.

| Allowed values |
|----------------|
| `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, `ADDITIONAL` |

**Example Request:**

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl -X POST https://api.upload-post.com/api/upload_photos \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user=my-profile' \
  -F 'platform[]=google_business' \
  -F 'photos[]=@/path/to/storefront.jpg' \
  -F 'gbp_location_id=accounts/123456789/locations/222222222' \
  -F 'gbp_post_type=MEDIA' \
  -F 'gbp_media_category=EXTERIOR'
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload_photos",
    headers={"Authorization": "Apikey your-api-key-here"},
    files=[("photos[]", open("/path/to/storefront.jpg", "rb"))],
    data={
        "user": "my-profile",
        "platform[]": "google_business",
        "gbp_location_id": "accounts/123456789/locations/222222222",
        "gbp_post_type": "MEDIA",
        "gbp_media_category": "EXTERIOR",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript

const form = new FormData();
form.append("photos[]", new Blob([fs.readFileSync("/path/to/storefront.jpg")]), "storefront.jpg");
form.append("user", "my-profile");
form.append("platform[]", "google_business");
form.append("gbp_location_id", "accounts/123456789/locations/222222222");
form.append("gbp_post_type", "MEDIA");
form.append("gbp_media_category", "EXTERIOR");

const response = await fetch("https://api.upload-post.com/api/upload_photos", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

**Success Response (200 OK):**

```json
{
  "success": true,
  "post_id": "accounts/123456789/locations/222222222/media/AF1QipN...",
  "url": "https://lh3.googleusercontent.com/...",
  "platform": "google_business",
  "post_type": "media",
  "media_category": "EXTERIOR",
  "media_format": "PHOTO"
}
```

| Field | Description |
|-------|-------------|
| `post_id` | The Google Business media resource name (`accounts/../locations/../media/..`). |
| `url` | The `googleUrl` of the uploaded media. |
| `post_type` | `"media"` — confirms the photo went to the Media/Gallery tab rather than to a Local Post. |
| `media_category` | The category the photo was filed under. |
| `media_format` | `"PHOTO"`. |

**Error Responses:**

*   `400 Bad Request` — invalid category:

    ```json
    {
      "success": false,
      "message": "Invalid gbp_media_category 'BANNER'. Allowed values: COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, PRODUCT, AT_WORK, FOOD_AND_DRINK, MENU, COMMON_AREA, ROOMS, TEAMS, ADDITIONAL",
      "error_code": "INVALID_MEDIA_CATEGORY"
    }
    ```

*   `400 Bad Request` — gallery request sent without any media:

    ```json
    {
      "success": false,
      "message": "A photo is required to upload to the Google Business media gallery",
      "error_code": "MEDIA_REQUIRED"
    }
    ```

    This is why a Media/Gallery request must carry an image. The same fields are accepted on `/api/upload` and `/api/upload_text`, but a request with no media attached will fail with `MEDIA_REQUIRED`.

### Example Requests

### Upload Photo and Video to Instagram (Carousel)

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'photos[]=@/path/to/image.jpg' \
  -F 'photos[]=@/path/to/video.mp4' \
  -F 'user="test"' \
  -F 'platform[]=instagram' \
  -F 'title="My Mixed Carousel"' \
  -X POST https://api.upload-post.com/api/upload_photos
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload_photos",
    headers={"Authorization": "Apikey your-api-key-here"},
    files=[
        ("photos[]", open("/path/to/image.jpg", "rb")),
        ("photos[]", open("/path/to/video.mp4", "rb")),
    ],
    data={
        "user": "test",
        "platform[]": "instagram",
        "title": "My Mixed Carousel",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript

const form = new FormData();
form.append("photos[]", new Blob([fs.readFileSync("/path/to/image.jpg")]), "image.jpg");
form.append("photos[]", new Blob([fs.readFileSync("/path/to/video.mp4")]), "video.mp4");
form.append("user", "test");
form.append("platform[]", "instagram");
form.append("title", "My Mixed Carousel");

const response = await fetch("https://api.upload-post.com/api/upload_photos", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

### Upload Photos to Facebook

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'photos[]=@/path/to/image1.jpg' \
  -F 'photos[]=@/path/to/image2.jpg' \
  -F 'user="test"' \
  -F 'platform[]=facebook' \
  -F 'facebook_page_id="123456789"' \
  -F 'title="My Photo Album"' \
  -X POST https://api.upload-post.com/api/upload_photos
```

### Upload Photo to Reddit

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'photos[]=@/path/to/image.jpg' \
  -F 'user="test"' \
  -F 'platform[]=reddit' \
  -F 'subreddit="pics"' \
  -F 'title="Check out this photo!"' \
  -X POST https://api.upload-post.com/api/upload_photos
```

### Responses

- 200 OK (synchronous, finished fast)

```json
{
  "success": true,
  "results": {
    "instagram": { "success": true, "url": "https://instagram.com/p/...", "photos_were_processed": true, "changes_per_image": [ {} ] },
    "reddit":   { "success": false, "error": "Subreddit is required for photo posts to Reddit." }
  },
  "usage": { "count": 13, "limit": 100, "last_reset": "..." }
}
```

- 200 OK (asynchronous/background started or sync→background fallback)

```json
{
  "success": true,
  "message": "Upload initiated successfully in background.",
  "request_id": "1a2b3c4d5e...",
  "total_platforms": 2
}
```

- 202 Accepted (scheduled)

```json
{
  "success": true,
  "job_id": "scheduler_job_456",
  "scheduled_date": "2025-09-22T10:00:00Z"
}
```

- 400 Bad Request
  - Missing `user`, `platform[]`, Pinterest without `pinterest_board_id`, Reddit without `subreddit`, invalid platforms, invalid `scheduled_date`.

- 401 Unauthorized: `{ "success": false, "message": "Invalid or expired token" }`

- 403 Forbidden (plan restrictions)

- 404 Not Found (e.g., user not found)

- 429 Too Many Requests (monthly limit exceeded; includes current usage)

```json
{
  "success": false,
  "message": "This upload would exceed your monthly limit.",
  "usage": { "count": 10, "limit": 10, "last_reset": "..." }
}
```

- 500 Internal Server Error: `{ "success": false, "error": "Detailed error message" }`

Notes
- When async or when sync falls back to background, use `GET /api/uploadposts/status?request_id={request_id}` to poll progress.
- Per-platform results may include fields like `url`, `post_id(s)`, and platform-specific metadata or `error`.


---
# Upload Status
URL: https://docs.upload-post.com/api/upload-status

# Upload Status

Check the status of asynchronous uploads initiated with `async_upload=true` or scheduled posts.

## Endpoint

```http
GET /api/uploadposts/status?request_id=yourrequestid
GET /api/uploadposts/status?job_id=yourjobid
```

## Headers

| Name | Value | Description |
|------|-------|-------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |

## Query Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| request_id | String | Conditional | The request identifier returned by the upload endpoints when `async_upload=true`. Use for async uploads. You can also provide your own `request_id` when submitting the upload (via form field or `X-Request-Id` header) so you can poll status even if the upload response is lost due to a timeout. |
| job_id | String | Conditional | The job identifier returned by scheduled posts. Use for posts with `scheduled_date`. |

**Note:** At least one of `request_id` or `job_id` must be provided.

## Behavior

- **Async uploads**: When you submit an upload request with `async_upload=true`, the API returns immediately with a `request_id`. Use this to retrieve aggregated progress and results.
- **Scheduled posts**: When you schedule a post with `scheduled_date`, the API returns a `job_id`. Use this to check the status after the scheduled time.
- The top-level `status` field may be one of:
  - `pending`: The request has been accepted but no platform results have been recorded yet. For scheduled posts, this means the job has not executed yet.
  - `queued`: The upload is queued and waiting for a worker to begin processing.
  - `processing`: At least one platform is actively being processed while others are still queued or pending.
  - `in_progress`: Some platform results have been recorded but not all.
  - `completed`: All platforms have finished successfully.
  - `failed`: All platforms have failed, or no activity has been recorded for over 1 hour.
  - `not_found`: No upload request was found with the provided ID (returned with HTTP 404).

- Individual platform results may have their own status:
  - `queued`: The platform upload is waiting to be processed.
  - `processing`: The platform upload is currently being processed.
  - `completed`: The platform upload finished successfully.
  - `failed`: The platform upload failed permanently.
  - `retryable`: The platform upload failed but is eligible for automatic retry.

## Example Request

**For async uploads:**

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  "https://api.upload-post.com/api/uploadposts/status?request_id=<REQUEST_ID>"
```

**For scheduled posts:**

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  "https://api.upload-post.com/api/uploadposts/status?job_id=<JOB_ID>"
```

## Example Response

**For async uploads (request_id) — in progress:**

```json
{
  "request_id": "7b2c2f5e-1234-4a6f-9f1d-a1b2c3d4e5f6",
  "status": "in_progress",
  "completed": 1,
  "total": 2,
  "results": [
    {
      "platform": "x",
      "success": true,
      "message": "Queued",
      "upload_timestamp": "2025-01-01T12:34:56Z"
    }
  ],
  "last_update": "2025-01-01T12:34:56Z"
}
```

**For scheduled posts (job_id):**

```json
{
  "job_id": "job_abc123xyz",
  "status": "completed",
  "completed": 2,
  "total": 2,
  "results": [
    {
      "platform": "x",
      "success": true,
      "message": "Published",
      "upload_timestamp": "2025-01-15T14:00:00Z"
    },
    {
      "platform": "linkedin",
      "success": true,
      "message": "Published",
      "upload_timestamp": "2025-01-15T14:00:05Z"
    }
  ],
  "last_update": "2025-01-15T14:00:05Z"
}
```

**Failed upload:**

```json
{
  "request_id": "ae8e8d98-dead-40bd-a206-8bacc9efea84",
  "status": "failed",
  "message": "Upload appears to have failed (no activity for over 1 hour)",
  "completed": 0,
  "total": 1,
  "results": []
}
```

**Not found:**

```json
{
  "request_id": "nonexistent-id",
  "status": "not_found",
  "message": "No upload request found with this ID"
}
```

## Responses

| Status | Description |
|--------|-------------|
| 200 OK | Success. Response includes `request_id` or `job_id` depending on which parameter was used. |
| 400 Bad Request | Missing both `request_id` and `job_id`: `{"error":"request_id or job_id is required"}` |
| 401 Unauthorized | Invalid or expired token |
| 404 Not Found | No upload request found with the provided ID |
| 500 Internal Server Error | Server error with details |

## SDK Examples

### Python

```python
from upload_post import UploadPostClient

client = UploadPostClient(api_key="your-api-key-here")

# Check status of an async upload
status = client.get_status("request_id_from_upload")

# Check status of a scheduled or queued post
status = client.get_job_status("job_id_from_scheduled_post")
```

### JavaScript/Node.js

```javascript

const client = new UploadPost('your-api-key-here');

// Check status of an async upload
const status = await client.getStatus('request_id_from_upload');

// Check status of a scheduled or queued post
const jobStatus = await client.getJobStatus('job_id_from_scheduled_post');
```

## Polling Best Practices

The status endpoint uses internal caching. Polling faster than the cache refresh interval returns the same result and wastes your rate limit budget.

| Status | Cache TTL | Recommended poll interval |
|--------|-----------|--------------------------|
| `queued` / `pending` | 2 seconds | Every 5–10 seconds |
| `processing` | 3 seconds | Every 10 seconds |
| `completed` / `failed` | 5 minutes | Stop polling — result is final |

For high-volume integrations, consider using [webhooks](./webhooks) instead of polling — you'll receive an instant `POST` notification when the upload completes.

See the full [Rate Limits & Polling guide](../guides/rate-limits) for detailed recommendations.

## Related

- [Text uploads](./upload-text)
- [Video uploads](./upload-video)
- [Photo uploads](./upload-photo)
- [Schedule posts](./schedule-posts)
- [Rate Limits & Polling](../guides/rate-limits)


---
# Upload Text
URL: https://docs.upload-post.com/api/upload-text

# Upload Text

Upload text posts to various social media platforms using this endpoint.

**Note:** Currently, this endpoint supports X (Twitter), LinkedIn, Facebook, Threads, Reddit, Bluesky, Discord, Telegram, and Google Business Profile. More platforms will be added in future updates.

### Endpoint

```
POST /api/upload_text
```

### Headers

| Name          | Value                      | Description                              |
|---------------|----------------------------|------------------------------------------|
| Authorization | Apikey your-api-key-here   | Your API key for authentication          |
| Idempotency-Key | unique-string | Optional. Prevents duplicate uploads if the same request is retried (e.g., after a timeout). Can also be sent as `X-Idempotency-Key` or `X-Request-Id`. When provided, if a matching upload job already exists, the API returns the existing job instead of creating a duplicate. |

### Common Parameters

| Name          | Type   | Required | Description                                                                |
|---------------|--------|----------|----------------------------------------------------------------------------|
| user          | String | Yes      | User identifier                                                            |
| platform[]    | Array  | Yes      | Platform(s) to upload to. Supported values: linkedin, x, facebook, threads, reddit, bluesky, discord, telegram, google_business, slack, mastodon, nostr, lemmy, devto, hashnode, wordpress, whop, listmonk |
| title         | String | Yes      | Default text content for the post.                                         |
| description    | String | No       | Optional extended body used only on Reddit (becomes the post text). Ignored elsewhere. |
| request_id | String | No | Client-provided request identifier. If omitted, the server generates one. Returned in every response and used to track the upload via [Upload Status](./upload-status). Useful when `async_upload=true` and the HTTP response might be lost (e.g., timeout). Can also be sent as an `X-Request-Id` header. |
| scheduled_date | String (ISO-8601) | No | Optional date/time (ISO-8601) to schedule publishing, e.g., "2024-12-31T23:45:00Z". Must be in the future (≤ 365 days). Omit for immediate upload. |
| timezone | String (IANA) | No | Optional timezone identifier (e.g., "Europe/Madrid", "America/New_York"). If provided, `scheduled_date` is interpreted in this timezone. Defaults to UTC if omitted. See [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for valid values. |
| async_upload  | Boolean | No      | If `true`, the request returns immediately with a `request_id` and processes in the background. See [Upload Status](./upload-status). |
| add_to_queue  | Boolean | No      | If `true`, automatically schedules the post to your next available queue slot. Cannot be used with `scheduled_date`. See [Queue System](./queue-system). |
| max_posts_per_slot | Integer | No | Override the profile's max posts per slot setting for this request. Only used when `add_to_queue=true`. See [Queue System](./queue-system). |
| first_comment | String | No       | Automatically post a first comment after publishing. Supported on Facebook, Threads, Bluesky, Reddit, X, YouTube, and LinkedIn. On X (Twitter) and Threads, this creates a reply to the main post (threading). On YouTube, it posts as a top-level comment. Note: Instagram does not support text-only posts, so this parameter is not applicable for Instagram here. |
| first_comment_media[] | File(s) | No | Image files to attach to the first comment as inline images. Currently supported on Reddit. Not available for scheduled or queued posts. |
| link_url      | String | No       | URL to include as a link preview card. When provided, platforms that support link previews (LinkedIn, Bluesky, Facebook, Reddit) will display a rich preview card with the page's title, description, and thumbnail image. Platform-specific parameters (`linkedin_link_url`, `bluesky_link_url`, `facebook_link_url`, `reddit_link_url`) take priority over this generic parameter. |

> **Important:** If you set `async_upload` to `false` but the upload takes longer than 59 seconds, it will automatically switch to asynchronous processing to avoid timeouts. In that case, use the `request_id` with the [Upload Status](./upload-status) endpoint to check the upload status and result.

> **Scheduling behavior:** When you provide `scheduled_date`, the API responds with `202 Accepted` and includes a `job_id`. That same `job_id` will later appear in [Upload History](./upload-history) to correlate the scheduled job with the publish record. You can also use the `job_id` with the [Upload Status](./upload-status) endpoint to check the execution status of the scheduled post.

This endpoint supports simultaneous text uploads to X (Twitter), LinkedIn, Facebook, Threads, Reddit, Bluesky, Discord, and Telegram.

### Platform-Specific First Comments

The `first_comment` parameter serves as a fallback. To set a custom first comment for a particular platform, use the optional `[platform]_first_comment` parameter. If provided, it will override the main `first_comment` for that platform.

**Example Optional Parameters:**

*   `facebook_first_comment`: "Let me know your thoughts in the comments!"
*   `x_first_comment`: "Thread incoming! 🧵"
*   `threads_first_comment`: "First comment on Threads!"
*   `reddit_first_comment`: "Source in the comments."
*   `bluesky_first_comment`: "More details in the replies."
*   `linkedin_first_comment`: "Source article in the comments."

### Platform-Specific Titles

The `title` parameter serves as a fallback. To set a custom title for a particular platform, use the optional `[platform]_title` parameter. If provided, it will override the main `title` for that platform.

**Example Optional Parameters:**

*   `linkedin_title`: "A professional insight on the latest industry trends."
*   `x_title`: "New update out now! 📢"
*   `facebook_title`: "Excited to share this with my Facebook friends."
*   `threads_title`: "Just posted something new on Threads!"

### Platform-Specific Parameters

### LinkedIn

| Name                    | Type   | Required | Description                                                                                                | Default     |
|-------------------------|--------|----------|------------------------------------------------------------------------------------------------------------|-------------|
| linkedin_title          | String | No       | Specific text for the LinkedIn post. Fallbacks to `title`.                                                 | `title`     |
| target_linkedin_page_id | String | No       | LinkedIn page ID to upload text to an organization's page. If not provided, posts to the user's personal profile. |             |
| linkedin_link_url       | String | No       | URL to include as a link preview card on the LinkedIn post. LinkedIn will display a rich preview with the page's title, description, and thumbnail. Overrides the generic `link_url` parameter for LinkedIn. | `link_url`  |

### X (Twitter)

:::warning URLs are stripped from every X post
Every URL that X would turn into a clickable link is removed from the caption,
title, and `first_comment` before the tweet is created — schemed URLs
(`https://`, `http://`, `ftp://`), `www.` hosts, shorteners (`t.co`, `bit.ly`,
…), bare hostnames with a path (`example.com/foo`), and IPs with a path.

Obfuscated forms (`example[.]com`, `hxxp://`, unicode dots) are **not**
stripped because X does not parse them as links — they display as plain text
and are billed at the normal `$0.015` rate.

**Why:** X charges `$0.200` per "Content: Create (with URL)" vs `$0.015` for
posts without a URL. Stripping happens on every X path (video, photo, text,
scheduled, retried) so usage stays on the cheap tier. See the
[Character Limits page](../resources/character-limits.md#x-twitter-character-limits)
for the full policy. To keep URLs in your X posts, see the
[X Links add-on](../guides/x-links-addon.md).
:::

| Name                        | Type    | Required | Description                                                                                                                           | Default     |
|-----------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------|-------------|
| `x_title`                     | String  | No       | Specific text for the tweet. Fallbacks to `title`. If the text is long, it will be split into a thread.                               | `title`     |
| `x_long_text_as_post`         | Boolean | No       | For X Premium users. When `true`, long text is published as a single post. When `false` (default), it creates a thread if text is long. | `false`     |
| `reply_settings`              | String  | No       | Controls who can reply to the tweet ("following", "mentionedUsers", "subscribers", "verified")                                       | -           |
| `quote_tweet_id`              | String  | No       | ID of the tweet to quote in a quote tweet. Mutually exclusive with `card_uri`, `poll_*`, and `direct_message_deep_link`. **Cannot be combined with media** (see note below). | -           |
| `geo_place_id`                | String  | No       | Place ID for adding geographic location to the tweet                                                                                  | -           |
| `nullcast`                    | Boolean | No       | Whether to publish without broadcasting (promotional/promoted-only posts)                                                             | `false`     |
| `for_super_followers_only`    | Boolean | No       | Tweet exclusive for super followers                                                                                                   | `false`     |
| `community_id`                | String  | No       | Community ID for posting to specific communities                                                                                      | -           |
| `share_with_followers`        | Boolean | No       | Share community post with followers                                                                                                   | `false`     |
| `direct_message_deep_link`    | String  | No       | Link to take the conversation from public timeline to private Direct Message. Mutually exclusive with `card_uri`, `quote_tweet_id`, and `poll_*`. | -           |
| `card_uri`                    | String  | No       | Card URI for Twitter Cards/ads/promoted content. Mutually exclusive with `quote_tweet_id`, `direct_message_deep_link`, and `poll_*`. | -           |
| `poll_options`                | Array   | No       | Array of poll options (2-4 options, max 25 characters each). Mutually exclusive with `card_uri`, `quote_tweet_id`, and `direct_message_deep_link`. | []          |
| `poll_duration`               | Integer | No       | Poll duration in minutes (5-10080, i.e., 5 minutes to 7 days)                                                                         | 1440        |
| `poll_reply_settings`         | String  | No       | Who can reply to poll ("following", "mentionedUsers", "subscribers", "verified"). Requires `poll_options`.                                          | -           |
| `reply_to_id`                 | String  | No       | ID of the tweet to reply to. Creates a reply to the specified tweet.                                                                                | -           |
| `exclude_reply_user_ids`      | Array   | No       | Array of user IDs to exclude from replying to this tweet. Requires `reply_to_id`. | []          |

Note: For Twitter uploads, specify the platform as `"x"` in the `platform[]` array.

:::warning Quote tweets cannot include media
`quote_tweet_id` and media are mutually exclusive on X. If you attach any
photo or video to a quote tweet, the request is rejected with:

> `quote_tweet_id cannot be used when uploading media to X. Remove it and retry.`

To quote a tweet **and** share an image, either post the quote tweet
without media, or upload the media as a separate (non-quote) tweet.
:::

:::info Replies can return 403 on the Pay-Per-Use tier
When you use `reply_to_id`, X may reject the reply with **HTTP 403** if the
connected account is on X's **Pay-Per-Use** tier and has **not engaged with
the author** you are replying to (it doesn't follow them / hasn't interacted
with them). This is an X platform restriction, not an authentication problem —
**reconnecting the account will not fix it**.

**Workaround:** reply to an author the connected account has already engaged
with (follows or has interacted with), or have the account engage with the
author first. The API surfaces this as a permission error rather than a
"session expired" prompt.
:::

#### How Twitter Threads Are Created

If your text in the `title` field is longer than 280 characters, our API automatically creates a Twitter thread. You don't need to do anything special. By default, `x_long_text_as_post` is `false`.

**How it works:**

Our system creates natural-looking threads by intelligently splitting your text:

1.  **It groups paragraphs:** The system combines as many paragraphs (text separated by a blank line) as possible into a single tweet without exceeding the character limit.
2.  **It splits long paragraphs:** If a single paragraph is too long for one tweet, it's split into smaller parts. The system first tries to split by line breaks and then by words.

This process ensures your threads are easy to read.

**Example of a thread creation**

If you send this text in the `title`:

```
This is the first paragraph. It is short.

This second paragraph is a bit longer. Our API tries to keep paragraphs together in one tweet.

This is a much longer third paragraph. It probably won't fit with the others. It might even be too long for a single tweet. If so, the API will split it. It will first look for line breaks. If a single line is still too long, it will split it by words. This creates a readable and well-structured Twitter thread automatically.
```

The API will create a thread like this:

**Tweet 1:**
> This is the first paragraph. It is short.
>
> This second paragraph is a bit longer. Our API tries to keep paragraphs together in one tweet.

**Tweet 2:**
> This is a much longer third paragraph. It probably won't fit with the others. It might even be too long for a single tweet. If so, the API will split it. It will first look for line breaks.

**Tweet 3:**
> If a single line is still too long, it will split it by words. This creates a readable and well-structured Twitter thread automatically.

### Facebook

| Name             | Type   | Required | Description                                                       | Default |
|------------------|--------|----------|-------------------------------------------------------------------|---------|
| facebook_title   | String | No       | Specific text for the Facebook post. Fallbacks to `title`.        | `title` |
| facebook_page_id | String | Yes      | Facebook Page ID where the text will be posted.                   | -       |
| facebook_link_url | String | No       | Optional URL to include for link preview in text posts. If provided, it's sent as `link` to the Graph API and Facebook may render a preview card. | -       |

> Note: If `facebook_page_id` is not provided, we will automatically use the user's only connected Page (if exactly one exists). If multiple Pages are connected, the API returns a helpful error with an `available_pages` list so you can choose one. Posting to personal Facebook profiles via API is not supported by Meta; only Pages can be posted to.

:::info Select the target Page on each Facebook upload
Connecting Facebook only links your account; it does **not** pick a destination
Page. You must select the target **Page on every upload** by passing
`facebook_page_id`. When exactly one Page is connected it is auto-selected;
when multiple Pages are connected and no `facebook_page_id` is given, the API
returns an `available_pages` list so you can choose. Use
[Get Facebook Pages](./get-facebook-pages.md) to look up the `page_id` values.
:::

### Threads

| Name                        | Type    | Required | Description                                                                                                                              | Default |
|-----------------------------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------------|---------|
| `threads_title`             | String  | No       | Specific text for the Threads post. Fallbacks to `title`.                                                                                | `title` |
| `threads_long_text_as_post` | Boolean | No       | If `true`, long text is published as a single post. If `false` (default), a thread is created if the text exceeds 500 characters.        | `false` |
| `threads_topic_tag`         | String  | No       | A topic tag for the post (1-50 characters). Cannot contain periods (.) or ampersands (&). One tag per post. Helps increase reach.        | -       |

Note: To upload content to Threads, specify the platform as `"threads"` in the `platform[]` array.

#### How Threads Are Created

If the text you provide exceeds 500 characters and `threads_long_text_as_post` is `false`, our API will automatically create a thread on Threads, similar to how it works with X (Twitter).

**How it works:**

Our system creates natural-looking threads by intelligently splitting your text:

1.  **It groups paragraphs:** The system combines as many paragraphs as possible into a single post without exceeding the character limit.
2.  **It splits long paragraphs:** If a single paragraph is too long for a post, it is split into smaller parts, first trying to break by line breaks, and if that's not enough, by words.

This process ensures that your Threads are coherent and easy to read, replicating the functionality you already enjoy for X.

### Reddit

| Name       | Type   | Required | Description                                                        | Default |
|------------|--------|----------|--------------------------------------------------------------------|---------|
| subreddit  | String | Yes      | Destination subreddit, without `r/` (e.g., `python`).              | -       |
| flair_id   | String | No       | ID of the flair template to apply to the post.                     | -       |
| reddit_link_url | String | No  | URL for creating a Reddit link post. When provided, creates a link post with a URL preview card instead of a self/text post. Overrides the generic `link_url` parameter for Reddit. | `link_url` |

If you provide the global `description` field, it becomes the Markdown body of the Reddit post; otherwise we post only the `title`.

> **Link posts:** When `reddit_link_url` (or the generic `link_url`) is provided, the post is created as a Reddit link post (`kind: link`) with a URL preview card. The `description` field is ignored for link posts since Reddit link posts don't have a text body.

Note: To upload content to Reddit, specify the platform as "reddit" in the `platform[]` array.

### Bluesky

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| bluesky_title | String | No | Specific text for the Bluesky post. Fallbacks to `title`. | `title` |
| bluesky_link_url | String | No | URL to include as a link preview card on the Bluesky post. Bluesky will display a rich external embed with the page's title, description, and thumbnail. Overrides the generic `link_url` parameter for Bluesky. | `link_url` |
| reply_to_id | String | No | URL or AT-URI of the post to reply to. Creates a reply to the specified post. | - |

Note: To upload content to Bluesky, specify the platform as "bluesky" in the `platform[]` array. The maximum character limit is 300 characters per post.

#### How Bluesky Threads Are Created

If your text exceeds 300 characters, our API automatically creates a Bluesky thread. This works similarly to X (Twitter) and Threads.

**How it works:**

Our system creates natural-looking threads by intelligently splitting your text:

1.  **It groups paragraphs:** The system combines as many paragraphs (text separated by a blank line) as possible into a single post without exceeding 300 characters.
2.  **It splits long paragraphs:** If a single paragraph is too long for one post, it's split into smaller parts. The system first tries to split by line breaks and then by words.

**Example of a thread creation**

If you send this text in the `title`:

```
This is the first paragraph. It is short.

This second paragraph is a bit longer. Our API tries to keep paragraphs together.

This is a much longer third paragraph. It probably won't fit with the others. If so, the API will split it into multiple posts automatically.
```

The API will create a thread like this:

**Post 1:**
> This is the first paragraph. It is short.
>
> This second paragraph is a bit longer. Our API tries to keep paragraphs together.

**Post 2:**
> This is a much longer third paragraph. It probably won't fit with the others. If so, the API will split it into multiple posts automatically.

### Discord

Discord posts go to the channel behind the incoming webhook you connected (see [Connecting Discord](../guides/connecting-accounts.md#connecting-discord-manual-credentials)). A text post is sent as the webhook message `content`.

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| discord_title | String | No | Specific text for the Discord message. Fallbacks to `title`. | `title` |

Note: To upload content to Discord, specify the platform as `"discord"` in the `platform[]` array. The maximum message length is **2000 characters**; longer text is truncated. Discord does not support analytics.

### Telegram

Telegram posts are delivered by your connected bot to the chat/channel you set up (see [Connecting Telegram](../guides/connecting-accounts.md#connecting-telegram-manual-credentials)). A text post is sent via `sendMessage`.

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| telegram_title | String | No | Specific text for the Telegram message. Fallbacks to `title`. | `title` |

Note: To upload content to Telegram, specify the platform as `"telegram"` in the `platform[]` array. The maximum message length is **4096 characters**; longer text is truncated. Telegram does not support analytics.

### Slack

Slack posts are delivered to the channel behind the Incoming Webhook you connected (see [Connecting Slack](../guides/connecting-accounts.md#connecting-slack-manual-credentials)). The text is sent as the webhook message.

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| slack_title | String | No | Specific text for the Slack message. Fallbacks to `title`. | `title` |

Note: To post to Slack, specify `"slack"` in the `platform[]` array. Slack is text-only (Incoming Webhooks cannot upload media). Max **40000** characters. Slack does not support analytics.

### Mastodon

Mastodon publishes a status ("toot") to your connected instance (see [Connecting Mastodon](../guides/connecting-accounts.md#connecting-mastodon-manual-credentials)).

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| mastodon_title | String | No | Specific text for the Mastodon status. Fallbacks to `title`. | `title` |

Note: To post to Mastodon, specify `"mastodon"` in the `platform[]` array. The default limit is **500** characters (configurable per instance). Mastodon also supports photos and video. Mastodon does not support analytics.

### Nostr

Nostr signs a note with your key and broadcasts it to your relays (see [Connecting Nostr](../guides/connecting-accounts.md#connecting-nostr-manual-credentials)).

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| nostr_title | String | No | Specific text for the Nostr note. Fallbacks to `title`. | `title` |

Note: To post to Nostr, specify `"nostr"` in the `platform[]` array. Nostr is text-only (media is referenced by URL, not uploaded). Max **32768** characters. Nostr does not support analytics.

### Lemmy

Lemmy creates a post in the community you connected (see [Connecting Lemmy](../guides/connecting-accounts.md#connecting-lemmy-manual-credentials)). Lemmy posts always require a title.

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| lemmy_title | String | No | Title of the Lemmy post. If omitted, it is derived from the first line of the content. Fallbacks to `title`. | `title` |

Note: To post to Lemmy, specify `"lemmy"` in the `platform[]` array. Title max **200** characters, body max **10000**. Lemmy also supports photo posts. Lemmy does not support analytics.

### Dev.to

Dev.to publishes a Markdown article (see [Connecting Dev.to](../guides/connecting-accounts.md#connecting-devto-manual-credentials)). The text content becomes the article body.

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| devto_title | String | No | Article title. If omitted, it is derived from the first line of the content. Fallbacks to `title`. | `title` |

Note: To post to Dev.to, specify `"devto"` in the `platform[]` array. Title max **250** characters, body (Markdown) max **250000**. Dev.to does not support analytics.

### Hashnode

Hashnode publishes a Markdown article to your publication (see [Connecting Hashnode](../guides/connecting-accounts.md#connecting-hashnode-manual-credentials)).

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| hashnode_title | String | No | Article title. If omitted, it is derived from the first line of the content. Fallbacks to `title`. | `title` |

Note: To post to Hashnode, specify `"hashnode"` in the `platform[]` array. Title max **250** characters, body (Markdown) max **100000**. Hashnode does not support analytics.

### WordPress

WordPress publishes a post to your site (see [Connecting WordPress](../guides/connecting-accounts.md#connecting-wordpress-manual-credentials)). The text content becomes the post body.

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| wordpress_title | String | No | Post title. If omitted, it is derived from the first line of the content. Fallbacks to `title`. | `title` |

Note: To post to WordPress, specify `"wordpress"` in the `platform[]` array. Title max **200** characters, body max **500000**. WordPress also supports photos and video. WordPress does not support analytics.

### Whop

Whop creates a post in the community forum ("experience") you connected (see [Connecting Whop](../guides/connecting-accounts.md#connecting-whop-manual-credentials)).

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| whop_title | String | No | Title of the Whop forum post. Fallbacks to `title`. | `title` |

Note: To post to Whop, specify `"whop"` in the `platform[]` array. Max **20000** characters. Posting requires an API key with the `forum:post:create` permission. Whop does not support analytics.

### Listmonk

Listmonk creates and sends an email campaign to the list you connected (see [Connecting Listmonk](../guides/connecting-accounts.md#connecting-listmonk-manual-credentials)).

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| listmonk_title | String | No | Email subject line. If omitted, it is derived from the first line of the content. Fallbacks to `title`. | `title` |

Note: To post to Listmonk, specify `"listmonk"` in the `platform[]` array. The text content becomes the email body. Subject max **500** characters, body max **200000**. Listmonk does not support analytics.

### Google Business Profile

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| gbp_location_id | String | No* | The location to post to. Use [Get Google Business Locations](./get-google-business-locations.md) to list available locations. | Auto |
| gbp_topic_type | String | No | Post type: `STANDARD` (default), `EVENT`, or `OFFER`. | `STANDARD` |
| gbp_cta_type | String | No | Call-to-action button: `BOOK`, `ORDER`, `SHOP`, `LEARN_MORE`, `SIGN_UP`, `CALL`. | - |
| gbp_cta_url | String | Conditional | URL for the CTA button. Required if `gbp_cta_type` is set. | - |
| gbp_post_type | String | No | Set to `MEDIA` (also accepts `PHOTO` or `GALLERY`) to publish an attached photo to the location's **Media/Gallery** tab instead of creating a Local Post. See [Publishing to the Media/Gallery tab](./upload-photo.md#publishing-to-the-mediagallery-tab). | - |
| gbp_upload_to_gallery | Boolean | No | Alternative to `gbp_post_type`. Set to `true` for the same Media/Gallery behaviour. | `false` |
| gbp_media_category | String | No | Category assigned to the uploaded photo in the Media tab. Alias: `media_category`. Allowed: `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, `ADDITIONAL`. | `ADDITIONAL` |

> Note: If `gbp_location_id` is not provided, the API will automatically use the account's only location (if exactly one exists). If multiple locations are connected, the API returns an error asking you to select one.

> Note: The Media/Gallery flow publishes a **photo**, so it is normally used from [`/api/upload_photos`](./upload-photo.md#publishing-to-the-mediagallery-tab). The fields are accepted here too, but a text-only request that selects the Media/Gallery tab with no media attached returns `400` with `error_code: MEDIA_REQUIRED`. Omitting `gbp_post_type` keeps the existing Local Post behaviour, unchanged.

**Event parameters** (when `gbp_topic_type` is `EVENT`):

| Name | Type | Required | Description |
|------|------|----------|-------------|
| gbp_event_title | String | Yes | Title of the event. |
| gbp_event_start_date | String | Yes | Start date in `YYYY-MM-DD` format. |
| gbp_event_start_time | String | No | Start time in `HH:MM` format (24h). |
| gbp_event_end_date | String | Yes | End date in `YYYY-MM-DD` format. |
| gbp_event_end_time | String | No | End time in `HH:MM` format (24h). |

**Offer parameters** (when `gbp_topic_type` is `OFFER`):

| Name | Type | Required | Description |
|------|------|----------|-------------|
| gbp_coupon_code | String | No | Coupon or promo code. |
| gbp_redeem_url | String | No | URL where the offer can be redeemed. |
| gbp_terms | String | No | Terms and conditions of the offer. |

### Example Requests

### Upload Text to X (Twitter)

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=x' \
  -F 'title="This is my tweet content!"' \
  -X POST https://api.upload-post.com/api/upload_text
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload_text",
    headers={"Authorization": "Apikey your-api-key-here"},
    data={
        "user": "test",
        "platform[]": "x",
        "title": "This is my tweet content!",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript
const form = new FormData();
form.append("user", "test");
form.append("platform[]", "x");
form.append("title", "This is my tweet content!");

const response = await fetch("https://api.upload-post.com/api/upload_text", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

### Create a Twitter Thread

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=x' \
  -F 'title="This is the first paragraph of a thread.\n\nThis is the second paragraph. Because this whole text is longer than 280 characters, the API will automatically create a thread. You can also add more paragraphs to create longer and more detailed threads easily."' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to LinkedIn with Link Preview

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=linkedin' \
  -F 'title="Check out this great article about renewable energy!"' \
  -F 'linkedin_link_url="https://example.com/article"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to LinkedIn (Personal Profile)

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=linkedin' \
  -F 'title="Exciting news to share on LinkedIn!"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to LinkedIn (Organization Page)

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=linkedin' \
  -F 'title="Our company is launching a new product!"' \
  -F 'target_linkedin_page_id="your_linkedin_page_id_here"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Facebook Page

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'title="This is a test post to Facebook using the title field for content!"' \
  -F 'user="test2"' \
  -F 'platform[]=facebook' \
  -F 'facebook_page_id="your_facebook_page_id_here"' \
  -F 'facebook_link_url="https://example.com/article"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Threads and Twitter (X)

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'title="This is a cross-post to Threads and X!"' \
  -F 'user="test"' \
  -F 'platform[]=threads' \
  -F 'platform[]=x' \
  -X POST https://api.upload-post.com/api/upload_text
  ```

### Upload Text to Reddit

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=reddit' \
  -F 'title="This is my Reddit post content!"' \
  -F 'subreddit="python"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Link Post to Reddit

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=reddit' \
  -F 'title="Check out this article!"' \
  -F 'subreddit="programming"' \
  -F 'reddit_link_url="https://example.com/article"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Reddit with First Comment and Images

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=reddit' \
  -F 'title="Discussion post"' \
  -F 'subreddit="pics"' \
  -F 'first_comment="Here are the source images:"' \
  -F 'first_comment_media[]=@/path/to/image1.jpg' \
  -F 'first_comment_media[]=@/path/to/image2.jpg' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Bluesky

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=bluesky' \
  -F 'title="This is my Bluesky post!"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Bluesky with Link Preview

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=bluesky' \
  -F 'title="Great read on climate policy!"' \
  -F 'bluesky_link_url="https://example.com/article"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Create a Bluesky Thread

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=bluesky' \
  -F 'title="This is the first paragraph of a thread.\n\nThis is the second paragraph. Because this whole text is longer than 300 characters, the API will automatically create a thread. You can add more paragraphs to create longer threads easily."' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Discord

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=discord' \
  -F 'title="Hello from Upload-Post!"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Telegram

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=telegram' \
  -F 'title="Hello from Upload-Post!"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Upload Text to Multiple Platforms with Link Preview

Use the generic `link_url` parameter to add a link preview card across all supported platforms at once:

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=linkedin' \
  -F 'platform[]=bluesky' \
  -F 'platform[]=facebook' \
  -F 'facebook_page_id="your_facebook_page_id_here"' \
  -F 'title="Check out our latest article on renewable energy!"' \
  -F 'link_url="https://example.com/article"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Reply to a Tweet on X (Twitter)

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=x' \
  -F 'title="This is my reply to a tweet!"' \
  -F 'reply_to_id="1346889436626259968"' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Reply to a Tweet on X (Twitter) with Excluded Users

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=x' \
  -F 'title="This is my reply to a tweet with some users excluded!"' \
  -F 'reply_to_id="1346889436626259968"' \
  -F 'exclude_reply_user_ids[]=1234567890' \
  -F 'exclude_reply_user_ids[]=0987654321' \
  -X POST https://api.upload-post.com/api/upload_text
```

### Responses

- 200 OK (synchronous, finished fast)

```json
{
  "success": true,
  "results": {
    "x":        { "success": true, "url": "https://x.com/..." },
    "facebook": { "success": false, "error": "Facebook Page ID is required for text posts to Facebook." }
  },
  "usage": { "count": 14, "limit": 100, "last_reset": "..." }
}
```

- 200 OK (asynchronous/background started or sync→background fallback)

```json
{
  "success": true,
  "message": "Text post initiated successfully in background.",
  "request_id": "1a2b3c4d5e...",
  "total_platforms": 1
}
```

- 202 Accepted (scheduled)

```json
{
  "success": true,
  "job_id": "scheduler_job_789",
  "scheduled_date": "2025-09-22T10:00:00Z"
}
```

- 400 Bad Request
  - Missing `title` (content), `user`, `platform[]`, invalid platforms, Reddit without `subreddit`, invalid `scheduled_date`. For Facebook without `facebook_page_id`, the per-platform result will include an error entry for `facebook`.

- 401 Unauthorized: `{ "success": false, "message": "Invalid or expired token" }`

- 403 Forbidden (plan restrictions)

- 404 Not Found (e.g., user not found)

- 429 Too Many Requests (monthly limit exceeded; includes current usage)

```json
{
  "success": false,
  "message": "This upload would exceed your monthly limit.",
  "usage": { "count": 10, "limit": 10, "last_reset": "..." }
}
```

- 500 Internal Server Error: `{ "success": false, "error": "Detailed error message" }`

Notes
- When async or when sync falls back to background, use `GET /api/uploadposts/status?request_id={request_id}` to poll progress.
- Per-platform results are returned under `results.{platform}` and may include fields like `url`, platform-specific IDs, or `error`.


---
# Upload Video
URL: https://docs.upload-post.com/api/upload-video

# Upload Video

Upload video to various social media platforms using this endpoint.

### Endpoint

```http
POST /api/upload
```

### Headers

| Name | Value | Description |
|------|-------|-------------|
| Authorization | Apikey your-api-key-here | Your API key for authentication |
| Idempotency-Key | unique-string | Optional. Prevents duplicate uploads if the same request is retried (e.g., after a timeout). Can also be sent as `X-Idempotency-Key` or `X-Request-Id`. When provided, if a matching upload job already exists, the API returns the existing job instead of creating a duplicate. |

### Common Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| user | String | Yes | User identifier |
| platform[] | Array | Yes | Platform(s) to upload to (e.g., "tiktok", "instagram", "linkedin", "youtube", "facebook", "twitter", "threads", "pinterest", "bluesky", "reddit", "discord", "telegram", "google_business", "mastodon", "wordpress") |
| video | File | Yes | The video file to upload (can be a file upload or a video URL) |
| title | String | Conditional | Default title of the video. **Required** for YouTube and Reddit. Optional for all other platforms (TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest). |
| description | String | No | Optional extended text used only on LinkedIn commentary, Facebook descriptions, YouTube descriptions, and Pinterest notes. Ignored elsewhere. |
| scheduled_date | String (ISO-8601) | No | Optional date/time (ISO-8601) to schedule publishing, e.g., "2024-12-31T23:45:00Z". Must be in the future (≤ 365 days). Omit for immediate upload. |
| timezone | String (IANA) | No | Optional timezone identifier (e.g., "Europe/Madrid", "America/New_York"). If provided, `scheduled_date` is interpreted in this timezone. Defaults to UTC if omitted. See [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for valid values. |
| request_id | String | No | Client-provided request identifier. If omitted, the server generates one. Returned in every response and used to track the upload via [Upload Status](./upload-status). Useful when `async_upload=true` and the HTTP response might be lost (e.g., timeout). Can also be sent as an `X-Request-Id` header. |
| async_upload | Boolean | No | If `true`, the request returns immediately with a `request_id` and processes in the background. See [Upload Status](./upload-status). |
| add_to_queue | Boolean | No | If `true`, automatically schedules the post to your next available queue slot. Cannot be used with `scheduled_date`. See [Queue System](./queue-system). |
| max_posts_per_slot | Integer | No | Override the profile's max posts per slot setting for this request. Only used when `add_to_queue=true`. See [Queue System](./queue-system). |
| first_comment | String | No | Automatically post a first comment after publishing. Supported on Instagram, Facebook, Threads, Bluesky, Reddit, X, YouTube, and LinkedIn. On X (Twitter) and Threads, this creates a reply to the main post. For X threads, the comment is posted as a reply to the last tweet in the thread. On YouTube, it posts as a top-level comment on the video. |
| first_comment_media[] | File(s) | No | Image files to attach to the first comment as inline images. Currently supported on Reddit. Not available for scheduled or queued posts. |

> **Important:** If you set `async_upload` to `false` but the upload takes longer than 59 seconds, it will automatically switch to asynchronous processing to avoid timeouts. In that case, use the `request_id` with the [Upload Status](./upload-status) endpoint to check the upload status and result.

> **Scheduling behavior:** When you provide `scheduled_date`, the API responds with `202 Accepted` and includes a `job_id`. That same `job_id` will later appear in [Upload History](./upload-history) so you can correlate the scheduled job with the eventual publish record. You can also use the `job_id` with the [Upload Status](./upload-status) endpoint to check the execution status of the scheduled post.

### Platform-Specific First Comments

The `first_comment` parameter serves as a fallback. To set a custom first comment for a particular platform, use the optional `[platform]_first_comment` parameter. If provided, it will override the main `first_comment` for that platform.

**Example Optional Parameters:**

*   `instagram_first_comment`: "Follow for more content! #photography"
*   `facebook_first_comment`: "Let me know your thoughts in the comments!"
*   `x_first_comment`: "Thread incoming! 🧵"
*   `threads_first_comment`: "First comment on Threads!"
*   `youtube_first_comment`: "Subscribe for more videos!"
*   `reddit_first_comment`: "Source in the comments."
*   `bluesky_first_comment`: "More details in the replies."
*   `linkedin_first_comment`: "Source article in the comments."

### Platform-Specific Titles

The `title` parameter serves as a fallback. To set a custom title for a particular platform, use the optional `[platform]_title` parameter. If provided, it will override the main `title` for that platform.

**Example Optional Parameters:**

* `instagram_title`: "Check out my latest reel on Instagram! #reels"
* `facebook_title`: "Excited to share this new video with my Facebook friends and family."
* `tiktok_title`: "New TikTok video just dropped! 🔥"
* `linkedin_title`: "A professional insight on the latest industry trends, discussed in this video."
* `x_title`: "New video out now! 📢"
* `youtube_title`: "My new YouTube video is live!"
* `pinterest_title`: "An inspiring video pin."
* `reddit_title`: "Check out this video!"

### Platform-Specific Parameters

### TikTok

For more information about Tiktok API parameters, visit the [Tiktok API documentation](https://developers.tiktok.com/doc/content-posting-api-reference-direct-post?enter_method=left_navigation).

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| tiktok_title | String | No | Specific title for the TikTok post (max 90 characters for photo posts, 2200 for video). Fallbacks to `title`. | `title` |
| privacy_level | String | No | Privacy setting ("PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY") | "PUBLIC_TO_EVERYONE" |
| disable_duet | Boolean | No | Disable duet feature | false |
| disable_comment | Boolean | No | Disable comments | false |
| disable_stitch | Boolean | No | Disable stitch feature | false |
| post_mode | String | No | `DIRECT_POST`: Directly post the content to TikTok user's account or `MEDIA_UPLOAD`: Upload content to TikTok for users to complete the post using TikTok's editing flow. Users will receive an inbox notification. | `DIRECT_POST` |
| cover_timestamp | Integer | No | Timestamp in milliseconds for video cover | 1000 |
| brand_content_toggle | Boolean| No       | Set to `true` for paid partnerships that promote third-party brands.        | false   |
| brand_organic_toggle | Boolean| No       | Set to `true` when promoting the creator's own business.                    | false   |
| is_aigc | Boolean | No | Indicates if content is AI-generated | false |

> **Recommended: Draft Mode (`post_mode=MEDIA_UPLOAD`) for better organic reach.** We recommend uploading TikTok videos in Draft mode. Publishing the video from inside the TikTok app (rather than posting directly through the API) tends to get more organic traffic, since videos posted natively by the user typically perform better in TikTok's distribution. Set `post_mode` to `MEDIA_UPLOAD`, then open the TikTok app to add the caption and publish.

> **Note on Draft Mode (`MEDIA_UPLOAD`):** When using `MEDIA_UPLOAD` mode (Draft), TikTok does not allow setting a title, caption, privacy settings, or other metadata via the API. The video is simply uploaded to your TikTok inbox/drafts, and you must add the title, caption, and settings manually within the TikTok app before publishing.

The global `description` field is ignored for TikTok uploads.

### Instagram

For more information about Instagram API parameters, visit the [Instagram Graph API documentation](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/media/).

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| instagram_title | String | No | Specific title for the Instagram post. Fallbacks to `title`. | `title` |
| media_type | String | No | Type of media ("REELS" or "STORIES") | "REELS" |
| share_mode | String | No | Reel posting mode. See Trial Reels below for details. | "CUSTOM" |
| share_to_feed | Boolean | No | Whether to share to feed (only for regular Reels, not Trial Reels) | true |
| collaborators | String | No | Comma-separated list of collaborator usernames (not available for Trial Reels) | - |
| cover_url | String | No | URL for custom video cover. You can also send a binary image via the `cover_image` field (see below). | - |
| cover_image | File | No | Binary cover image file (JPEG, ≤ 8MB). Uploaded to a public URL automatically. If both `cover_image` and `cover_url` are provided, `cover_url` takes precedence. | - |
| audio_name | String | No | Name of the audio track embedded in your video | - |
| user_tags | String | No | Users to tag on the Reel. Accepts a comma-separated list (e.g., `"@user1, user2"`) — Instagram does **not** require coordinates for video tags. | - |
| location_id | String | No | Instagram location ID | - |
| thumb_offset | String | No | Timestamp offset for video thumbnail, expressed in milliseconds | - |

> **Tagging on photo posts is different.** For `/upload_photos`, Instagram requires `x`/`y` coordinates in a JSON array and silently drops username-only tags. See [Upload Photo → Instagram `user_tags`](./upload-photo.md#note-on-instagram-user_tags-for-photo-posts).

#### Trial Reels (share_mode)

Trial Reels allow you to test content with non-followers first to see how it performs before sharing with your followers. This feature is available for public Instagram accounts with at least 1,000 followers.

**Available `share_mode` values:**

| Value | Description |
|-------|-------------|
| `CUSTOM` | Regular Reel (default) - Shown to all followers immediately |
| `TRIAL_REELS_SHARE_TO_FOLLOWERS_IF_LIKED` | Trial Reel with auto-share - Shown to non-followers first. If it performs well within 72 hours, Instagram automatically shares it with your followers |
| `TRIAL_REELS_DONT_SHARE_TO_FOLLOWERS` | Trial Reel without auto-share - Shown only to non-followers. You decide later in the Instagram app if you want to share with followers |

**Important notes about Trial Reels:**
- Only you can see that a Reel is marked as a Trial. To everyone else, it appears as a regular Reel.
- Your followers won't see the Trial on your profile or in their feeds unless you (or Instagram, if auto-share is enabled) choose to share it.
- Collaborators cannot be added to Trial Reels.
- There may be limits on how many Trial Reels you can publish within a certain period.

#### Note on Instagram `audio_name`

- **Scope**: Reels only, and only for the original audio embedded in your uploaded video. It does not let you pick licensed/trending music from Instagram’s library via API.
- **Limit**: You can rename only once (when creating the Reel via API, or later from the audio page if you are the audio owner).
- **Behavior**: The Reel is published using the audio embedded in your video and displays the name you provide in `audio_name`.

The global `description` field is ignored for Instagram video uploads.

### LinkedIn

For more information about LinkedIn API parameters, visit the [LinkedIn Marketing API documentation](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api?view=li-lms-2025-02&tabs=http).

| Name                    | Type   | Required | Description                                          | Default     |
|-------------------------|--------|----------|------------------------------------------------------|-------------|
| linkedin_title          | String | No       | Specific title for the LinkedIn post. Fallbacks to `title`. | `title` |
| linkedin_description or description    | String | No       | Sent as the LinkedIn commentary. If omitted, we reuse `title`. | `title` |
| visibility              | String | Yes      | Visibility setting ("CONNECTIONS", "PUBLIC", "LOGGED_IN", "CONTAINER") | "PUBLIC"    |
| target_linkedin_page_id | String | No       | LinkedIn page ID to upload videos to an organization | "107579166" |

### YouTube

For more information about YouTube API parameters, visit the [YouTube Data API documentation](https://developers.google.com/youtube/v3/docs/videos?hl=es-419#resource).

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| youtube_title | String | No | Specific title for the YouTube video. Fallbacks to `title`. | `title` |
| youtube_description or description | String | No | Populates `snippet.description`. If omitted, we send `title`. | `title` |
| tags | Array | No | Array of tags | [] |
| categoryId | String | No | Video category | "22" |
| privacyStatus | String | No | Privacy setting ("public", "unlisted", "private") | "public" |
| embeddable | Boolean | No | Whether video is embeddable | true |
| license | String | No | Video license ("youtube", "creativeCommon") | "youtube" |
| publicStatsViewable | Boolean | No | Whether public stats are viewable | true |
| thumbnail | File | No | Custom thumbnail image to set after upload. Accepts a multipart image file or a public URL. Formats: JPG/PNG/GIF/BMP. Max 2 MB. If both `thumbnail` (file) and `thumbnail_url` are provided, the file takes precedence. YouTube custom thumbnails are not supported for Shorts; they only apply to standard YouTube videos. | - |
| thumbnail_url | String (URL) | No | Alternative to provide the thumbnail as a public URL. | - |
| selfDeclaredMadeForKids | Boolean | No | Explicit declaration that the video is made for children | false |
| containsSyntheticMedia | Boolean | No | Declaration that the video contains synthetic or AI-generated content | false |
| defaultLanguage | String | No | Language of title and description (BCP-47 code, e.g., "es", "en") | - |
| defaultAudioLanguage | String | No | Language of the video audio (BCP-47 code, e.g., "es-ES", "en-US") | - |
| allowedCountries | String | No | Comma-separated list of country codes where the video is allowed (e.g., "US,CA,MX") | - |
| blockedCountries | String | No | Comma-separated list of country codes where the video is blocked (e.g., "CN,RU") | - |
| hasPaidProductPlacement | Boolean | No | Declaration that the video includes paid product placements | false |
| recordingDate | String | No | Recording date and time of the video (ISO 8601 format, e.g., "2024-01-15T14:30:00Z") | - |
| youtube_playlist_id | String | No | One or more playlist IDs on the same channel to add the video to after it is published. Accepts a single ID (e.g., "PLxxxxxxxxxxxx") or a comma-separated list (e.g., "PLaaa,PLbbb"). Adding to a playlist is best-effort: if it fails the video stays published and the per-playlist outcome is reported under `playlists` in the response. | - |
| youtube_subtitle_file | File | No | Subtitle file to upload to the video. Supported formats: SRT, VTT, SBV, SUB, ASS, SSA, TTML, DFXP. Must be accompanied by `youtube_subtitle_language`. | - |
| youtube_subtitle_language | String | No | BCP-47 language code for the subtitle file (e.g., "en", "es", "fr"). Required when uploading subtitles. | - |
| youtube_subtitle_name | String | No | Display name for the subtitle track (e.g., "English", "Español"). Defaults to the language code if not provided. | - |
| `youtube_subtitle_file_{N}` | File | No | Indexed subtitle file for multiple tracks (N = 0, 1, 2...). Use with `youtube_subtitle_language_{N}`. | - |
| `youtube_subtitle_language_{N}` | String | No | Language code for indexed subtitle track N. | - |
| `youtube_subtitle_name_{N}` | String | No | Display name for indexed subtitle track N. | - |

> **Important:** YouTube custom thumbnails are not supported for Shorts; they only apply to standard YouTube videos.

**Notes about YouTube parameters:**
- **Subtitles:** You can upload multiple subtitle files by using indexed fields (`youtube_subtitle_file_0`, `youtube_subtitle_language_0`, `youtube_subtitle_file_1`, `youtube_subtitle_language_1`, etc.). Each subtitle file requires a corresponding language code. Subtitles are uploaded after the video is processed using the [YouTube Captions API](https://developers.google.com/youtube/v3/docs/captions).
- **Region restrictions:** `allowedCountries` and `blockedCountries` cannot be used simultaneously. Country codes must be ISO 3166-1 alpha-2 (e.g., "US", "CA", "MX").
- **Language settings:** `defaultLanguage` affects title and description display, while `defaultAudioLanguage` specifies the spoken language in the video. Use BCP-47 codes (e.g., "es" for Spanish, "es-ES" for Spain Spanish).
- **Legal declarations:** `selfDeclaredMadeForKids` is used for COPPA compliance. `containsSyntheticMedia` provides transparency for AI-generated content. `hasPaidProductPlacement` ensures FTC compliance.
- **Playlists:** `youtube_playlist_id` adds the video to one or more playlists **after** it has been published, using the [YouTube PlaylistItems API](https://developers.google.com/youtube/v3/docs/playlistItems/insert). Pass a single playlist ID or a comma-separated list of IDs. The playlists must belong to the same channel as the connected YouTube account. This step is non-fatal: if adding to a playlist fails (e.g., an invalid ID or a playlist owned by another channel), the video remains published and the outcome for each playlist is returned in the `playlists` array of the response (each entry includes `success` and `playlist_id`, plus `playlist_item_id` on success or `error` on failure).

### Facebook

For more information about Facebook API parameters, visit the [Facebook Graph API documentation](https://developers.facebook.com/docs/graph-api/reference/page/video_reels/?locale=es_ES) and the [Facebook Video API Publishing Guide](https://developers.facebook.com/docs/video-api/guides/publishing/).

| Name             | Type   | Required | Description                                                       | Default     |
|------------------|--------|----------|-------------------------------------------------------------------|-------------|
| facebook_title   | String | No       | Specific title for the Facebook post. Fallbacks to `title`. **Note:** If `facebook_media_type` is `"STORIES"`, this field is ignored.       | `title` |
| facebook_description or description      | String | No       | Sent as `description` for the video. **Note:** If `facebook_media_type` is `"STORIES"`, this field is ignored. | `title` |
| facebook_page_id | String | Yes      | Facebook Page ID where the video will be posted                   | -           |
| facebook_media_type | String | No | Type of media: `"REELS"` (short-form 9:16), `"STORIES"` (24h ephemeral), or `"VIDEO"` (normal page video, any aspect ratio, up to 4 hours) | "REELS" |
| video_state      | String | No       | Desired state of the video ("DRAFT", "PUBLISHED")    | "PUBLISHED" |
| thumbnail_url    | String | No       | Public URL of an image to set as the video thumbnail. Only supported when `facebook_media_type` is `"VIDEO"`. Uses the [Facebook Video Thumbnails API](https://developers.facebook.com/docs/graph-api/reference/video/thumbnails/#Creating). | -           |

> **Normal page videos (`VIDEO`):** Use `facebook_media_type=VIDEO` to upload regular videos to a Facebook Page (not Reels or Stories). These videos have no forced 9:16 aspect ratio, support durations up to 4 hours, and support custom thumbnails via the `thumbnail_url` parameter.

> Note: If `facebook_page_id` is not provided, we will automatically use the user's only connected Page (if exactly one exists). If multiple Pages are connected, the API returns a helpful error with an `available_pages` list so you can choose one. Posting to personal Facebook profiles via API is not supported by Meta; only Pages can be posted to.

:::info Select the target Page on each Facebook upload
Connecting Facebook only links your account; it does **not** pick a destination
Page. You must select the target **Page on every upload** by passing
`facebook_page_id`. When exactly one Page is connected it is auto-selected;
when multiple Pages are connected and no `facebook_page_id` is given, the API
returns an `available_pages` list so you can choose. Use
[Get Facebook Pages](./get-facebook-pages.md) to look up the `page_id` values.
:::

### Threads

For more information about Threads API parameters, visit the [Threads API documentation](https://developers.facebook.com/docs/threads).

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| threads_title | String | No | Specific title for the Threads post. Fallbacks to `title`. | `title` |
| threads_topic_tag | String | No | A topic tag for the post (1-50 characters). Cannot contain periods (.) or ampersands (&). One tag per post. Helps increase reach. | - |

The global `description` field is ignored for Threads video uploads.

### X (Twitter)

:::warning URLs are stripped from every X post
Upload-Post removes every URL that X would turn into a clickable link from
the caption, title, and `first_comment` before sending the tweet — schemed
URLs (`https://`, `http://`, `ftp://`), `www.` hosts, shorteners (`t.co`,
`bit.ly`, …), bare hostnames with a path (`example.com/foo`), and IPs with a
path.

Obfuscated forms (`example[.]com`, `hxxp://`, unicode dots) are **not**
stripped because X does not parse them as links — they display as plain text
and are billed at the normal `$0.015` rate.

**Why:** X bills `$0.200` per post that contains a URL vs `$0.015` without —
13× more. Stripping happens on every X path (video, photo, text, scheduled
and retried). See the
[Character Limits page](../resources/character-limits.md#x-twitter-character-limits)
for details.
:::

For more information about X API parameters, visit the [X API Post Creation documentation](https://docs.x.com/x-api/posts/creation-of-a-post).

| Name                        | Type    | Required | Description                                                                                                                           | Default     |
|-----------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------|-------------|
| x_title                      | String  | No       | Specific title for the tweet. Fallbacks to `title`.                                                                                   | `title`     |
| x_long_text_as_post          | Boolean | No       | When `true`, publishes long text as a single post. Otherwise, creates a thread.                                                      | `false`     |
| reply_settings               | String  | No       | Controls who can reply to the tweet ("following", "mentionedUsers", "subscribers", "verified")                                       | -           |
| geo_place_id                 | String  | No       | Place ID for adding geographic location to the tweet                                                                                  | -           |
| nullcast                     | Boolean | No       | Whether to publish without broadcasting (promotional/promoted-only posts)                                                             | `false`     |
| for_super_followers_only     | Boolean | No       | Tweet exclusive for super followers                                                                                                   | `false`     |
| community_id                 | String  | No       | Community ID for posting to specific communities                                                                                      | -           |
| share_with_followers         | Boolean | No       | Share community post with followers                                                                                                   | `false`     |
| direct_message_deep_link     | String  | No       | Link to take the conversation from public timeline to private Direct Message                                                         | -           |
| tagged_user_ids              | Array   | No       | Array of user IDs to tag in the media (max 10 users)                                                                                  | []          |
| reply_to_id                  | String  | No       | ID of the tweet to reply to. Creates a reply to the specified tweet.                                                                  | -           |
| exclude_reply_user_ids       | Array   | No       | Array of user IDs to exclude from replying to this tweet. Requires `reply_to_id`.                                                    | []          |

The global `description` field is ignored for X uploads.

:::warning Quote tweets cannot include media
Because video uploads always attach media, `quote_tweet_id` cannot be used on
this endpoint. X treats media and `quote_tweet_id` as mutually exclusive, so a
video post that also sets `quote_tweet_id` is rejected with:

> `quote_tweet_id cannot be used when uploading media to X. Remove it and retry.`

To quote a tweet, post it as a text-only quote tweet via [Upload Text](./upload-text.md)
and share the video as a separate tweet.
:::

:::info Replies can return 403 on the Pay-Per-Use tier
When you use `reply_to_id`, X may reject the reply with **HTTP 403** if the
connected account is on X's **Pay-Per-Use** tier and has **not engaged with the
author** being replied to (doesn't follow them / hasn't interacted with them).
This is an X platform restriction, not an auth problem — reconnecting will not
fix it. **Workaround:** reply to an author the account has already engaged with.
:::

#### How X (Twitter) Thread Creation Works (Advanced Logic)

**Note:** The following describes the default thread creation logic. To override this and post long text as a single post, set the `x_long_text_as_post` parameter to `true`.

The system is engineered to create well-formatted, natural-looking threads on X (formerly Twitter). Instead of simply splitting text at every line break, it intelligently groups paragraphs to create more readable tweets.

Here's the step-by-step logic:

**Intelligent Paragraph Grouping (Primary Method):**

The function first identifies distinct paragraphs (any text separated by a blank line).
It then combines as many of these paragraphs as possible into a single tweet, filling it up to the 280-character limit without exceeding it. The double newline (`\n\n`) between combined paragraphs is preserved for formatting.
This results in fewer, more substantial tweets that flow naturally, just as if a person had written them.

**Handling Exceptionally Long Paragraphs:**

If a single paragraph is, by itself, longer than the 280-character limit, a more granular splitting logic is automatically triggered for that paragraph only:

- **Split by Line Break:** The system first attempts to break the paragraph down by its individual line breaks (`\n`).
- **Split by Word:** If any of those single lines are still too long, it will split them by words as a final resort.

**Media Attachment:**

For posts that include photos or videos, all media is attached only to the first tweet of the thread. The subsequent tweets in the thread will be text-only replies.

### Pinterest

| Name                                   | Type   | Required | Description                                                                              | Default |
|----------------------------------------|--------|----------|------------------------------------------------------------------------------------------|---------|
| pinterest_title                        | String | No       | Specific title for the Pinterest Pin. Fallbacks to `title`.                              | `title` |
| pinterest_description or description    | String | No       | Populates `pin.description`. If omitted, we reuse `title`.                                | `title` |
| pinterest_board_id                     | String | Yes       | Pinterest board ID to publish the video to.                                              | -       |
| pinterest_alt_text   | String | No       | Alt text for the video.                      | -       |
| pinterest_link                         | String | No       | Destination link for the video Pin.                                                      | -       |
| pinterest_cover_image_url              | String | No       | URL of an image to use as the video cover.                                               | -       |
| pinterest_cover_image_content_type     | String | No       | Content type of the cover image (e.g., image/jpeg, image/png), used if `pinterest_cover_image_data` is provided. | -       |
| pinterest_cover_image_data             | String | No       | Base64 encoded cover image data, used if `pinterest_cover_image_content_type` is provided. | -       |
| pinterest_cover_image_key_frame_time | Integer| No       | Time in milliseconds of the video frame to use as cover.                                 | -       |

### Bluesky

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| bluesky_title | String | No | Specific text for the Bluesky video post. Fallbacks to `title`. | `title` |

Note: Video uploads to Bluesky are limited to 10GB per day/user and 25 videos per day, 100MBs Maximum, and up to 3 minutes (180 seconds) in duration. Supported formats: .mp4, .mpeg, .webm, .mov.

### Reddit

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| reddit_title | String | No | Specific title for the Reddit post. Fallbacks to `title`. | `title` |
| subreddit | String | Yes | Name of the subreddit to post to (without "r/"). | - |
| flair_id | String | No | ID of the flair to apply to the post. | - |

### Discord

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| discord_title | String | No | Caption sent alongside the video. Fallbacks to `title`. | `title` |

Note: Discord uploads the video as a message attachment to the channel behind the connected webhook (see [Connecting Discord](../guides/connecting-accounts.md#connecting-discord-manual-credentials)). The optional caption is limited to 2000 characters. File size is bounded by the limit of the target Discord server.

### Telegram

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| telegram_title | String | No | Caption sent alongside the video. Fallbacks to `title`. | `title` |

Note: Your connected bot delivers the video to the configured chat/channel via `sendVideo` (see [Connecting Telegram](../guides/connecting-accounts.md#connecting-telegram-manual-credentials)). The optional caption is limited to **1024 characters**.

### Mastodon

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| mastodon_title | String | No | Caption for the Mastodon post. Fallbacks to `title`. | `title` |

Note: The video is uploaded to your instance (async processing) and attached to the status once ready (see [Connecting Mastodon](../guides/connecting-accounts.md#connecting-mastodon-manual-credentials)).

### WordPress

| Name | Type | Required | Description | Default |
|---|---|---|---|---|
| wordpress_title | String | No | Post title. Fallbacks to `title`. | `title` |

Note: The video is uploaded to the WordPress media library and embedded in the post (see [Connecting WordPress](../guides/connecting-accounts.md#connecting-wordpress-manual-credentials)).

### Google Business Profile

| Name | Type | Required | Description | Default |
|------|------|----------|-------------|---------|
| gbp_location_id | String | No* | The location to post to. Use [Get Google Business Locations](./get-google-business-locations.md) to list available locations. | Auto |
| gbp_topic_type | String | No | Post type: `STANDARD` (default), `EVENT`, or `OFFER`. | `STANDARD` |
| gbp_cta_type | String | No | Call-to-action button: `BOOK`, `ORDER`, `SHOP`, `LEARN_MORE`, `SIGN_UP`, `CALL`. | - |
| gbp_cta_url | String | Conditional | URL for the CTA button. Required if `gbp_cta_type` is set. | - |
| gbp_post_type | String | No | Set to `MEDIA` (also accepts `PHOTO` or `GALLERY`) to publish an attached photo to the location's **Media/Gallery** tab instead of creating a Local Post. See [Publishing to the Media/Gallery tab](./upload-photo.md#publishing-to-the-mediagallery-tab). | - |
| gbp_upload_to_gallery | Boolean | No | Alternative to `gbp_post_type`. Set to `true` for the same Media/Gallery behaviour. | `false` |
| gbp_media_category | String | No | Category assigned to the uploaded photo in the Media tab. Alias: `media_category`. Allowed: `COVER`, `PROFILE`, `LOGO`, `EXTERIOR`, `INTERIOR`, `PRODUCT`, `AT_WORK`, `FOOD_AND_DRINK`, `MENU`, `COMMON_AREA`, `ROOMS`, `TEAMS`, `ADDITIONAL`. | `ADDITIONAL` |

> Note: If `gbp_location_id` is not provided, the API will automatically use the account's only location (if exactly one exists). If multiple locations are connected, the API returns an error asking you to select one.

> Note: The Media/Gallery flow publishes a **photo**. A request that selects it without any media attached returns `400` with `error_code: MEDIA_REQUIRED`. Omitting `gbp_post_type` keeps the existing Local Post behaviour, unchanged.

**Event parameters** (when `gbp_topic_type` is `EVENT`):

| Name | Type | Required | Description |
|------|------|----------|-------------|
| gbp_event_title | String | Yes | Title of the event. |
| gbp_event_start_date | String | Yes | Start date in `YYYY-MM-DD` format. |
| gbp_event_start_time | String | No | Start time in `HH:MM` format (24h). |
| gbp_event_end_date | String | Yes | End date in `YYYY-MM-DD` format. |
| gbp_event_end_time | String | No | End time in `HH:MM` format (24h). |

**Offer parameters** (when `gbp_topic_type` is `OFFER`):

| Name | Type | Required | Description |
|------|------|----------|-------------|
| gbp_coupon_code | String | No | Coupon or promo code. |
| gbp_redeem_url | String | No | URL where the offer can be redeemed. |
| gbp_terms | String | No | Terms and conditions of the offer. |

### Example Requests

### Upload a Video to TikTok

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="Your Video Title"' \
  -F 'user="test"' \
  -F 'platform[]=tiktok' \
  -X POST https://api.upload-post.com/api/upload
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload",
    headers={"Authorization": "Apikey your-api-key-here"},
    files={"video": open("/path/to/your/video.mp4", "rb")},
    data={
        "title": "Your Video Title",
        "user": "test",
        "platform[]": "tiktok",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript

const form = new FormData();
form.append("video", new Blob([fs.readFileSync("/path/to/your/video.mp4")]), "video.mp4");
form.append("title", "Your Video Title");
form.append("user", "test");
form.append("platform[]", "tiktok");

const response = await fetch("https://api.upload-post.com/api/upload", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

### Upload a Video to YouTube Using URL

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video="https://example.com/videos/myvideo.mp4"' \
  -F 'title="Your Video Title"' \
  -F 'description="Your video description"' \
  -F 'user="test"' \
  -F 'platform[]=youtube' \
  -F 'tags[]=tutorial' \
  -F 'tags[]=howto' \
  -F 'categoryId="22"' \
  -X POST https://api.upload-post.com/api/upload
```

### Upload a Video to YouTube With Custom Thumbnail

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="Your Video Title"' \
  -F 'description="Your video description"' \
  -F 'user="test"' \
  -F 'platform[]=youtube' \
  -F 'thumbnail_url="https://example.com/images/thumbnail-1280x720.jpg"' \
  -X POST https://api.upload-post.com/api/upload
```

### Upload to YouTube with thumbnail file

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H "Authorization: Apikey <API_KEY>" \
  -F "user=<profile_username>" \
  -F "platform[]=youtube" \
  -F "title=Demo video" \
  -F "description=Description" \
  -F "video=@/path/video.mp4;type=video/mp4" \
  -F "thumbnail=@/path/thumbnail.jpg;type=image/jpeg"
```

### Upload to YouTube with subtitle files

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H "Authorization: Apikey <API_KEY>" \
  -F "user=<profile_username>" \
  -F "platform[]=youtube" \
  -F "title=Demo video with subtitles" \
  -F "description=Video with English and Spanish subtitles" \
  -F "video=@/path/video.mp4;type=video/mp4" \
  -F "youtube_subtitle_file_0=@/path/subtitles_en.srt" \
  -F "youtube_subtitle_language_0=en" \
  -F "youtube_subtitle_name_0=English" \
  -F "youtube_subtitle_file_1=@/path/subtitles_es.srt" \
  -F "youtube_subtitle_language_1=es" \
  -F "youtube_subtitle_name_1=Español"
```

### Upload to YouTube and add to playlists

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H "Authorization: Apikey <API_KEY>" \
  -F "user=<profile_username>" \
  -F "platform[]=youtube" \
  -F "title=Demo video added to playlists" \
  -F "description=Video that is added to one or more playlists after publishing" \
  -F "video=@/path/video.mp4;type=video/mp4" \
  -F "youtube_playlist_id=PLaaaaaaaaaaaa,PLbbbbbbbbbbbb"
```

### Responses

- 200 OK (synchronous, finished fast)

```json
{
  "success": true,
  "results": {
    "instagram": {
      "success": true,
      "url": "https://instagram.com/p/...",
      "container_id": "1789...",
      "video_was_transcoded": true,
      "changes": {},
      "prevalidation_metadata": {}
    },
    "linkedin": {
      "success": false,
      "error": "Expired access token"
    }
  },
  "usage": {
    "count": 12,
    "limit": 100,
    "last_reset": "2025-09-01T10:00:00.000Z"
  }
}
```

- 200 OK (asynchronous/background started, including sync→background fallback)

```json
{
  "success": true,
  "message": "Upload initiated successfully in background.",
  "request_id": "1a2b3c4d5e...",
  "total_platforms": 3
}
```

- 202 Accepted (scheduled)

```json
{
  "success": true,
  "job_id": "scheduler_job_123",
  "scheduled_date": "2025-09-22T10:00:00Z"
}
```

- 400 Bad Request
  - Missing `user`, `platform[]`, video file/URL, invalid `scheduled_date`, invalid platform values, Pinterest without `pinterest_board_id`.

```json
{ "success": false, "message": "Username required in form data" }
```

- 401 Unauthorized

```json
{ "success": false, "message": "Invalid or expired token" }
```

- 403 Forbidden (e.g., TikTok on Free plan)

```json
{ "success": false, "message": "TikTok uploads are not available on the Free plan. Please upgrade to a paid plan." }
```

- 404 Not Found (e.g., user not found after auth)

```json
{ "success": false, "message": "User not found" }
```

- 429 Too Many Requests (monthly limit exceeded; includes current usage)

```json
{
  "success": false,
  "message": "This upload would exceed your monthly limit.",
  "usage": { "count": 10, "limit": 10, "last_reset": "..." }
}
```

- 500 Internal Server Error

```json
{ "success": false, "error": "Detailed error message" }
```

Notes
- When async or when sync falls back to background, use `GET /api/uploadposts/status?request_id={request_id}` to poll progress.
- Per-platform results may include: `url`, `publish_id`, `container_id`, `post_id`, `video_urn`, `video_reel_id`, `video_id`, `image_urns`, `post_ids`, `video_was_transcoded`, `changes`, `prevalidation_metadata`, or `error`.


---
# User Profiles API
URL: https://docs.upload-post.com/api/user-profiles

# User Profiles API

These endpoints allow you to integrate Upload-Post directly into your platform by managing user profiles and generating secure tokens for account linking.

See the [User Profile Integration Guide](../guides/user-profile-integration.md) for a conceptual overview and workflow.

## Authentication

All API requests require authentication using your API Key. Include it in the `Authorization` header for every request:

```
Authorization: Apikey YOUR_API_KEY
```

Replace `YOUR_API_KEY` with the actual API key provided to you.

---

## User Profile Management

Manage user profiles within Upload-Post that correspond to users on your platform.

### Endpoint

```
/api/uploadposts/users
```

---

### Create User Profile

Creates a new profile linked to a user on your platform.

*   **Method:** `POST`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name     | Type   | Required | Description                                                                 |
    |----------|--------|----------|-----------------------------------------------------------------------------|
    | username | String | Yes      | A unique identifier for the user on your platform (e.g., your internal ID). |

*   **Example Body:**
    ```json
    {
      "username": "your_platform_user_id_123"
    }
    ```
*   **Success Response (201 Created):**
    ```json
    {
      "profile": {
        "created_at": "Fri, 02 May 2025 21:43:14 GMT",
        "social_accounts": {
          "tiktok": ""
          // Other platforms will appear here as they are connected
        },
        "username": "your_platform_user_id_123"
      },
      "success": true
    }
    ```
    *   `profile`: Contains details of the newly created profile.
        *   `created_at`: Timestamp of profile creation.
        *   `social_accounts`: Object showing connected accounts (initially empty or with placeholders).
        *   `username`: The unique identifier provided.
    *   `success`: Indicates successful creation.
*   **Error Responses:**
    *   `400 Bad Request`: Missing or invalid `username`.
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `403 Forbidden`: Profile limit reached for the current plan (`error_code: PROFILE_LIMIT_REACHED`).
    *   `409 Conflict`: A profile with the provided `username` already exists.

---

### Get User Profiles

Retrieves a list of all user profiles created under your API key.

*   **Method:** `GET`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
*   **Query Parameters:** None
*   **Success Response (200 OK):**
    ```json
    {
        "limit": 10,
        "plan": "default",
        "profiles": [
            {
                "created_at": "2025-04-02T17:44:33.229755",
                "social_accounts": {
                    "facebook": {
                        "display_name": "FB User",
                        "social_images": "url_to_fb_image"
                    },
                    "instagram": {
                        "display_name": "IG User",
                        "social_images": "url_to_ig_image"
                    }
                    // ... other connected platforms with their details
                },
                "username": "your_platform_user_id_1"
            },
            {
                "created_at": "Fri, 02 May 2025 21:43:14 GMT",
                "social_accounts": {
                    "tiktok": "" // Example of a platform added but not yet connected
                },
                "username": "your_platform_user_id_2"
            }
        ],
        "success": true
    }
    ```
    *   `limit`: The maximum number of profiles allowed by the current plan.
    *   `plan`: The subscription plan associated with the API key.
    *   `profiles`: An array of user profile objects.
        *   `created_at`: Timestamp of profile creation.
        *   `social_accounts`: An object detailing connected social media accounts. Each key is the platform name (e.g., `facebook`, `instagram`, `tiktok`). The value can be an object with details (`display_name`, `social_images`, `username`) or an empty string/null if not fully connected.
        *   `username`: The unique identifier for the profile.
    *   `success`: Indicates successful retrieval.
*   **Error Responses:**
    *   `401 Unauthorized`: Invalid or missing API Key.

---

### Get a Specific User Profile

Retrieves information for a single user profile using its username.

*   **Method:** `GET`
*   **Endpoint**: `/api/uploadposts/users/{username}`

**Path Parameters**

| Parameter  | Type   | Description                                            |
| :--------- | :----- | :----------------------------------------------------- |
| `username` | string | **Required**. The username of the profile to retrieve. |

**Success Response (200 OK)**

If the profile is found, the API will return a JSON object with the profile details.

```json
{
    "success": true,
    "profile": {
        "created_at": "2023-10-27T10:00:00Z",
            "social_accounts": {
                "tiktok": {
                    "username": "tiktok_user_123",
                    "display_name": "User Display Name",
                    "social_images": "https://example.com/image.jpg"
                },
                "bluesky": {
                     "username": "user.bsky.social",
                     "display_name": "Bluesky User",
                     "social_images": "https://example.com/avatar.jpg"
                },
                "instagram": null
            },
        "username": "specific_profile_name"
    }
}
```

**Error Response (404 Not Found)**

If no profile is found with the specified `username`, the API will return:

```json
{
    "success": false,
    "message": "Profile not found"
}
```

---

### Delete User Profile

Deletes an existing user profile and its associated data (like social connections).

*   **Method:** `DELETE`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name     | Type   | Required | Description                                      |
    |----------|--------|----------|--------------------------------------------------|
    | username | String | Yes      | The unique identifier of the profile to delete. |

*   **Example Body:**
    ```json
    {
      "username": "user_id_to_delete"
    }
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "message": "Perfil eliminado correctamente",
      "success": true
    }
    ```
*   **Error Responses:**
    *   `400 Bad Request`: Missing or invalid `username`.
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: No profile found with the provided `username`.

---

## JWT Management

Generate and validate JWTs for the secure social account linking process.

### Endpoint: Generate JWT URL

```
/api/uploadposts/users/generate-jwt
```

Generates a secure, single-use URL containing a JWT. Your user visits this URL to link their social media accounts.

*   **Method:** `POST`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name         | Type    | Required | Description                                                                                      |
    |--------------|---------|----------|--------------------------------------------------------------------------------------------------|
    | username     | String  | Yes      | The identifier for the user profile for which the JWT is being generated.                        |
    | redirect_url | String  | No       | (Optional) The URL to which the user will be redirected after linking their social account.      |
    | logo_image   | String  | No       | (Optional) A URL to a logo image to display on the linking page for branding purposes.           |
    | redirect_button_text | String | No | (Optional) The text to display on the redirect button after linking. Defaults to "Logout connection". |
    | connect_title | String | No | (Optional) Custom title text for the connection page. Defaults to "Connect Social Media Accounts". |
    | connect_description | String | No | (Optional) Custom description text for the connection page. Defaults to "Connect your social media accounts to manage your posts.". |
    | platforms    | Array   | No       | (Optional) List of platforms to show for connection. Possible values: 'tiktok', 'instagram', 'linkedin', 'youtube' (not working, waiting for audit), 'facebook', 'x', 'threads', 'google_business'. Defaults to all supported platforms. |
    | show_calendar | Boolean | No       | (Optional) Whether to show the calendar view on the connection page. Defaults to `true`.         |
    | readonly_calendar | Boolean | No   | (Optional) When `true`, shows only a read-only calendar view. The user cannot edit, delete, or create posts, and cannot connect or disconnect social accounts. Ideal for sharing a content calendar with end clients. Defaults to `false`. |
    | language | String | No | (Optional) Forces the language of the connection page for this profile. Supported values: `en`, `es`, `de`, `fr`, `pt`, `pl`, `tr`. When omitted, the page automatically detects the visitor's browser language and falls back to English. |
    | ui_labels | Object | No | (Optional) Flat object of i18n key → replacement string, to override individual pieces of connect-page UI text. See [Custom UI Labels](#custom-ui-labels). |

*   **Supported languages:**

    | Value | Language   |
    |-------|------------|
    | `en`  | English    |
    | `es`  | Spanish    |
    | `de`  | German     |
    | `fr`  | French     |
    | `pt`  | Portuguese |
    | `pl`  | Polish     |
    | `tr`  | Turkish    |

*   **Example Body:**
    ```json
    {
      "username": "your_platform_user_id_123"
    }
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "access_url": "https://app.upload-post.com/connect?token=GENERATED_JWT_TOKEN",
      "success": true,
      "duration": "48h"
    }
    ```
    *   `access_url`: The secure URL your user needs to visit. Redirect your user to this URL.
    *   `success`: Always `true` if the request was successful.
    *   `duration`: The validity period of the generated JWT (48 hours).
*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_platform_user_id_123"}'
    ```
*   **Example Request with Calendar Disabled (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_platform_user_id_123", "show_calendar": false}'
    ```
*   **Example Request with Read-Only Calendar for Clients (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_platform_user_id_123", "readonly_calendar": true, "logo_image": "https://youragency.com/logo.png", "connect_title": "Your Content Calendar"}'
    ```
    This generates a link where the client only sees the calendar with scheduled posts (social channel, date/time, visual, text) but cannot edit anything or access other sections.
*   **Example Success Response (200 OK):**
    ```json
    {
      "access_url": "https://app.upload-post.com/connect?token=GENERATED_JWT_TOKEN_STRING",
      "duration": "48h",
      "success": true
    }
    ```
*   **Calendar Deep Link:** If you want users to land directly on the shared calendar view, replace the path with `/connect/calendar` while keeping the token intact, e.g. `https://app.upload-post.com/connect/calendar?token=GENERATED_JWT_TOKEN`. The page will automatically fall back to `/connect` when the profile has `show_calendar` disabled. When `readonly_calendar` is `true`, the user is automatically redirected to the calendar view regardless of the URL path.

*   **Error Responses:**
    *   `400 Bad Request`: Missing or invalid `username`, or an invalid `ui_labels` payload (see [Custom UI Labels](#custom-ui-labels)).
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `403 Forbidden`: Profile exists but is blocked by plan limits (`error_code: PROFILE_BLOCKED`).
    *   `404 Not Found`: No profile found with the provided `username` (`error_code: PROFILE_NOT_FOUND`).

*   **Integration tip:** If JWT generation returns `404`, call `GET /api/uploadposts/users` first to confirm the profile exists and that profile creation did not fail due to plan limits.

#### Custom UI Labels {#custom-ui-labels}

`connect_title`, `connect_description` and `redirect_button_text` cover the three most visible strings on the connection page. `ui_labels` goes further: it lets a white-label integration override **any individual piece of connect-page UI text**, in any language, without waiting for a translation to ship.

`ui_labels` is a **flat** object mapping the connect page's own i18n dot-path keys to the replacement strings:

```json
{
  "username": "your_platform_user_id_123",
  "language": "tr",
  "ui_labels": {
    "connect.connectButton": "Bağlan",
    "connect.notConnected": "Bağlı değil"
  }
}
```

*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "username": "your_platform_user_id_123",
        "language": "tr",
        "ui_labels": {
          "connect.connectButton": "Bağlan",
          "connect.notConnected": "Bağlı değil"
        }
      }'
    ```

**Validation rules**

| Rule | Limit |
|------|-------|
| Maximum number of entries | 100 |
| Key format | Must match `^[a-zA-Z0-9_.]+$` — letters, numbers, dots and underscores only |
| Value type | Must be a string |
| Value length | Maximum 300 characters |

A violation returns `400 Bad Request` with the specific reason:

```json
{
  "success": false,
  "message": "Invalid ui_labels key 'bad key!'. Keys may only contain letters, numbers, dots and underscores"
}
```

```json
{
  "success": false,
  "message": "ui_labels['connect.x'] must be a string"
}
```

**Update semantics**

`ui_labels` is stored on the profile and persists across JWT generations:

| You send | Result |
|----------|--------|
| The field is **omitted** | Previously stored labels are left **untouched** |
| `"ui_labels": { ... }` (non-empty) | Replaces the stored labels with what you sent |
| `"ui_labels": null` | **Clears** all stored labels |
| `"ui_labels": {}` | **Clears** all stored labels |

The stored labels are returned inside the `profile` object by [`GET /api/uploadposts/users/validate-jwt`](#endpoint-validate-jwt), so you can read back what is currently applied.

:::warning
The keys are the **connect page's own translation keys**, not free-form identifiers. An unknown key is simply never rendered. Override only keys you have confirmed exist — verify each one against the live connect page after setting it.
:::

#### Mobile OAuth Compatibility

When users open the connection page on a mobile device (iOS or Android), the
operating system may intercept OAuth URLs (e.g. `instagram.com`, `accounts.google.com`)
and open the corresponding native app instead of keeping the flow in the browser.
Because native apps cannot handle the OAuth authorization URL, the connection fails.

Upload-Post automatically detects mobile browsers and routes OAuth redirects through
a secure intermediate page (`/api/uploadposts/oauth/bounce`) that performs the
redirect via JavaScript. This bypasses Universal Links (iOS) and App Links (Android)
interception so the OAuth flow stays entirely in the mobile browser.

**No action is required from API consumers** — the mobile-safe redirect is applied
automatically when the user accesses the `access_url` on a mobile device.

---

### Endpoint: Validate JWT

```
/api/uploadposts/users/validate-jwt
```

(Optional) Allows you to validate a JWT token. The primary validation occurs automatically when the user accesses the `access_url`.

*   **Method:** `GET`
*   **Headers:**
    *   `Authorization: Bearer YOUR_JWT_TOKEN`
*   **Body Parameters:** None. The token is read from the `Authorization` header, not from the request body.
*   **Example Request (curl):**
    ```bash
    # Replace YOUR_JWT_TOKEN with the actual token string
    curl -X GET https://api.upload-post.com/api/uploadposts/users/validate-jwt \
      -H "Authorization: Bearer YOUR_JWT_TOKEN"
    ```
*   **Success Response (200 OK - Valid Token):** Returns the profile details associated with the token.
    ```json
    {
      "profile": {
        "social_accounts": {
          "tiktok": null,
          "instagram": "connected_account_details",
          // ... other platforms
        },
        "username": "your_platform_user_id_123",
        "ui_labels": {
          "connect.connectButton": "Bağlan",
          "connect.notConnected": "Bağlı değil"
        }
      },
      "success": true
    }
    ```
    *   `profile`: Contains details about the user profile linked to the token.
        *   `social_accounts`: An object showing the connection status for various platforms (e.g., `null` if not connected, or details if connected).
        *   `username`: The unique identifier provided when the profile was created.
        *   `ui_labels`: The connect-page text overrides currently stored for this profile, as set via [`generate-jwt`](#custom-ui-labels). Empty or absent when no overrides are stored.
    *   `success`: Indicates the token is valid.
*   **Success Response (200 OK - Invalid Token):**
    ```json
    {
      "isValid": false,
      "reason": "Token expired or invalid signature" // Example reason
    }
    ```
*   **Error Responses:**
    *   `401 Unauthorized`: Invalid, expired, or missing JWT token in the `Authorization` header.

---

## Facebook Pages

Retrieve Facebook page IDs associated with user profiles to enable posting to Facebook pages.

### Endpoint

```
/api/uploadposts/facebook/pages
```

---

### Get Facebook Pages

Fetches Facebook page IDs associated with a profile. You can use this endpoint to connect and start posting on Facebook pages.

*   **Method:** `GET`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
*   **Query Parameters:**

    | Name    | Type   | Required | Description                                                                          |
    |---------|--------|----------|--------------------------------------------------------------------------------------|
    | profile | String | No       | The unique identifier of the profile. If not specified, returns all pages for your account. |

*   **Example Request (curl):**
    ```bash
    curl 'https://api.upload-post.com/api/uploadposts/facebook/pages?profile=your_profile' \
      -H 'Authorization: Apikey YOUR_API_KEY'
    ```
*   **Example Request (without profile parameter):**
    ```bash
    curl 'https://api.upload-post.com/api/uploadposts/facebook/pages' \
      -H 'Authorization: Apikey YOUR_API_KEY'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "pages": [
        {
          "page_id": "123456789",
          "page_name": "My Business Page",
          "profile": "your_platform_user_id_123"
        },
        {
          "page_id": "987654321", 
          "page_name": "Another Page",
          "profile": "your_platform_user_id_123"
        }
      ],
      "success": true
    }
    ```
    *   `pages`: Array of Facebook page objects associated with the profile(s).
        *   `page_id`: The Facebook page ID that can be used for posting.
        *   `page_name`: The display name of the Facebook page.
        *   `profile`: The profile identifier associated with this page.
    *   `success`: Indicates successful retrieval.
*   **Error Responses:**
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: No profile found with the provided identifier (if profile parameter is specified).

---

## Manual Credential Connections

Most platforms are connected through the [JWT connection page](#endpoint-generate-jwt-url) (OAuth). A few platforms instead use a **manual credential** model: you submit the credentials directly to a dedicated endpoint, Upload-Post validates them against the platform, encrypts the secret at rest, and links it to the profile. There is **no OAuth flow and no browser redirect**. Tokens/webhooks do not expire, so no reconnection is required unless you revoke them.

### Connect Discord

Links a Discord channel **incoming webhook** to a profile. Upload-Post validates the webhook by performing a `GET` on the webhook URL (expecting a `200` with the webhook `id`, `channel_id`, and `guild_id`), then encrypts and stores the webhook URL.

*   **Method:** `POST`
*   **Endpoint:** `/api/uploadposts/users/discord/credentials`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name             | Type   | Required | Description                                                                 |
    |------------------|--------|----------|-----------------------------------------------------------------------------|
    | profile_username | String | Yes      | The profile to link the Discord webhook to.                                 |
    | webhook_url      | String | Yes      | The Discord channel incoming webhook URL (`https://discord.com/api/webhooks/...`). |
    | name             | String | No       | Optional display name for the connection. Defaults to the webhook's name.   |

*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/discord/credentials \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "profile_username": "your_platform_user_id_123",
        "webhook_url": "https://discord.com/api/webhooks/123456789/abcdef...",
        "name": "My Server #announcements"
      }'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "success": true,
      "message": "Discord credentials saved successfully"
    }
    ```
    On success, the profile's `social_accounts.discord` is set to the connection key (the supplied `name`, or the webhook id when `name` is omitted).
*   **Error Responses:**
    *   `400 Bad Request`: Missing `profile_username`/`webhook_url`, an invalid webhook URL format, or a webhook that fails validation (deleted/invalid).
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: User or profile not found.

To find the webhook URL: open Discord → **Server Settings → Integrations → Webhooks → New Webhook**, pick the target channel, and click **Copy Webhook URL**.

### Connect Telegram

Links a **Telegram bot** (bring-your-own bot) and a target chat/channel to a profile. Upload-Post validates the bot token via `getMe` and the chat via `getChat` (the bot must be an **admin** of the chat), then encrypts and stores the bot token.

*   **Method:** `POST`
*   **Endpoint:** `/api/uploadposts/users/telegram/credentials`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name             | Type   | Required | Description                                                                                  |
    |------------------|--------|----------|----------------------------------------------------------------------------------------------|
    | profile_username | String | Yes      | The profile to link the Telegram bot to.                                                     |
    | bot_token        | String | Yes      | The bot token from @BotFather (e.g. `123456:ABC-DEF...`).                                     |
    | chat_id          | String | Yes      | The target chat: a public channel `@username`, or a numeric chat id (e.g. `-100123456789`).  |
    | name             | String | No       | Optional display name for the connection. Defaults to the bot's username.                    |

*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/telegram/credentials \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "profile_username": "your_platform_user_id_123",
        "bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
        "chat_id": "@my_channel",
        "name": "My Channel"
      }'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "success": true,
      "message": "Telegram credentials saved successfully"
    }
    ```
    On success, the profile's `social_accounts.telegram` is set to the connection key (the supplied `name`, or the bot username when `name` is omitted).
*   **Error Responses:**
    *   `400 Bad Request`: Missing `profile_username`/`bot_token`/`chat_id`, an invalid bot token, or a chat that is not reachable (often because the bot is not an admin of the chat).
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: User or profile not found.

To set up the bot: message [@BotFather](https://t.me/BotFather) → `/newbot` to get a `bot_token`, then add the bot to your target channel/group **as an administrator** so it can post.

---


---
# Video Format Requirements
URL: https://docs.upload-post.com/api/video-requirements

# Video Format Requirements

This document outlines the video format requirements for uploading to various social media platforms via the API.

## Automatic Video Transformation

Our API automatically transforms videos to adapt them to the specifications of each social network. However, if you use this feature, the upload will take longer because the transformation is performed first before uploading to the platforms. 

If you prefer faster uploads, you can pre-process your videos according to the specific requirements of each platform outlined in this document.

## Video Encoding Compatibility

Some video creation tools occasionally produce videos with encoding that Meta's systems don't accept. At times, their output needs to be re-encoded for compatibility.

### One Solution: Re-encode with FFmpeg

If your video uploads are failing, try re-encoding the video using FFmpeg, an open-source tool for video processing:

```bash
ffmpeg -i your_original_video.mp4 -c:v libx264 -preset medium -profile:v high -level 4.0 -pix_fmt yuv420p -c:a aac -movflags +faststart meta_compatible_video.mp4
```

This command converts your video to use the widely-compatible H.264 video codec and AAC audio codec, which Meta platforms accept.

Re-encoding "normalizes" your video to use standard encoding parameters that Meta's platforms are designed to process, without sacrificing quality. If you see these errors regularly, this simple step can save you frustration when sharing your creative content.

### FFmpeg Installation and Usage

**Installation instructions:**
- **macOS:** `brew install ffmpeg`
- **Windows:** `winget install ffmpeg`
- **Linux:** `sudo apt install ffmpeg` (Ubuntu/Debian) or `sudo dnf install ffmpeg` (Fedora)

**Parameters:**
- `-c:v libx264`: Uses H.264 video codec
- `-preset medium`: Balance between encoding speed and quality
- `-profile:v high -level 4.0`: Compatibility settings
- `-pix_fmt yuv420p`: Standard pixel format for maximum compatibility
- `-b:v 5000k`: Video bitrate (adjust as needed for quality)
- `-c:a aac`: AAC audio codec
- `-b:a 192k`: Audio bitrate
- `-movflags +faststart`: Optimizes file for web streaming

## TikTok Video Requirements

- **Supported Formats:** MP4 (recommended), WebM, MOV
- **Supported Codecs:** H.264 (recommended), H.265, VP8
- **Framerate:** Minimum: 23 FPS, Maximum: 60 FPS
- **Picture Size:** Minimum: 360 pixels (height and width), Maximum: 4096 pixels (height and width)
- **Duration:** Maximum via API: 10 minutes. (Note: All TikTok creators can post 3-minute videos. Some creators have access to 5-minute or 10-minute videos. Users may trim videos in the TikTok app.)
- **Size:** Maximum: 4GB

## Instagram Video Requirements

- **Container Format:** MOV or MP4 (MPEG-4 Part 14)
  - No edit lists
  - Moov atom at the head of the file
- **Audio Codec:** AAC
  - Maximum sampling rate: 48 kHz
  - 1 or 2 channels (mono or stereo)
  - Audio bitrate: 128 kbps
- **Video Codec:** HEVC or H264
  - Progressive scan
  - Closed GOP
  - Chroma subsampling: 4:2:0
  - Video bitrate: VBR, maximum 25 Mbps
- **Frame Rate:** 23-60 FPS
- **Image Size:**
  - Maximum horizontal pixels: 1,920
  - Aspect ratio: 0.01:1 to 10:1
  - Recommended aspect ratio: 9:16 (to avoid cropping or white space)
- **Duration & Size:**
  - Maximum duration: 15 minutes (Instagram increased from 90 seconds)
  - Minimum duration: 3 seconds
  - Maximum file size: 300 MB

> **Note:** Instagram now supports Reels up to 15 minutes in duration. The previous 90-second limit has been removed.

## YouTube Video Requirements

- **File Size:** Maximum: 256 GB
- **Accepted MIME Types:** `video/*`, `application/octet-stream`

> **Important:** Custom thumbnails are not supported for YouTube Shorts; they only apply to standard YouTube videos.

## LinkedIn Video Requirements

- **File Size:** Minimum: 75 KB, Maximum: 5 GB
- **Duration:** Minimum: 3 seconds, Maximum: 10 minutes
- **Resolution:**
  - Range: 256 x 144 to 4,096 x 2,304
  - Aspect ratio: 1:2.4 to 2.4:1
- **Technical Specs:**
  - Frame rate: 10-60 fps
  - Bitrate: 192 kbps - 30 Mbps
- **Supported Formats:** AAC, ASF, FLV, MP3, MP4, MPEG-1, MPEG-4, MKV, WebM, H264/AVC, Vorbis, VP8, VP9, WMV2, WMV3

## Facebook Video Requirements

### Reels

- **File Format:** MP4 (recommended)
- **Resolution & Aspect Ratio:**
  - Recommended: 1080 x 1920 pixels
  - Minimum: 540 x 960 pixels
  - Aspect ratio: 9:16
- **Duration:**
  - 3-90 seconds
  - Maximum 60 seconds for page stories
- **Video Settings:**
  - Frame rate: 24-60 fps
  - Chroma subsampling: 4:2:0
  - Closed GOP (2-5 seconds)
  - Compression: H.264, H.265, VP9, AV1
  - Progressive scan
- **Audio Settings:**
  - Bitrate: 128 kbps+
  - Channels: Stereo
  - Codec: AAC (low complexity)
  - Sample rate: 48 kHz

### Normal Page Videos

Use `facebook_media_type=VIDEO` to upload regular videos to a Facebook Page. Normal page videos have more permissive requirements than Reels:

- **File Format:** MP4 (recommended), MOV, AVI, and other common formats
- **Resolution:** Up to 4096x4096 pixels, any aspect ratio
- **Duration:** Up to 4 hours
- **File Size:** Up to 10 GB
- **Thumbnails:** Custom thumbnails are supported via the `thumbnail_url` parameter. See the [Facebook Video Thumbnails API](https://developers.facebook.com/docs/graph-api/reference/video/thumbnails/#Creating).

## X (Twitter) Video Requirements

- **Recommended Codec & Profile:**
  - Video: H264 High Profile
  - Audio: AAC LC (Low Complexity)
- **Frame Rates:**
  - Recommended: 30 FPS, 60 FPS
  - Maximum: 60 FPS
- **Resolution:**
  - Recommended: 1280x720 (landscape), 720x1280 (portrait), 720x720 (square)
  - Dimensions: 32x32 to 1920x1920
- **Bitrate:**
  - Minimum Video: 5,000 kbps
  - Minimum Audio: 128 kbps
- **Aspect Ratio:**
  - Recommended: 16:9 (landscape/portrait), 1:1 (square)
  - Range: 1:3 to 3:1
  - Pixel Aspect Ratio: 1:1
- **Duration & File Size:**
  - Duration: 0.5 seconds - 14,400 seconds (4 hours) for Premium
  - Max File Size: 1 GB+ for Premium/Amplify
- **Technical Video Specs:**
  - Pixel Format: YUV 4:2:0
  - GOP: Must not be open
  - Scan Type: Progressive scan
- **Technical Audio Specs:**
  - Channels: Mono or Stereo (not 5.1 or greater)
  - High-Efficiency AAC: Not supported
- **Custom Thumbnails:** Supported for Premium/Amplify users

## Threads Video Requirements

- **Container:** MOV or MP4
  - No edit lists
  - `moov` atom at the front
- **Audio Codec:** AAC
  - 48kHz sample rate maximum
  - 1 or 2 channels (mono/stereo)
  - Bitrate: 128 kbps
- **Video Codec:** HEVC or H264
  - Progressive scan
  - Closed GOP
  - 4:2:0 chroma subsampling
- **Frame Rate:** 23-60 FPS
- **Picture Size:**
  - Max columns (horizontal pixels): 1920
  - Aspect ratio: 0.01:1 to 10:1 (9:16 recommended)
- **Video Bitrate:** VBR, 100 Mbps maximum
- **Duration:**
  - Max: 300 seconds (5 minutes)
  - Min: > 0 seconds
- **File Size:** 1 GB maximum

## Pinterest Video Requirements

- **File Size:** Maximum: 1 GB
- **Supported Formats:** MP4, MOV, M4V
- **Duration:** Minimum: 4 seconds, Maximum: 15 minutes
- **Aspect Ratio:** Taller than 1.91:1 and shorter than 1:2. Recommended for standard video: 1:1 (square) or 2:3, 4:5 or 9:16 (vertical)

## Reddit Video Requirements

- **File Size:** Maximum: 1 GB
- **Supported Formats:** MP4, MOV
- **Duration:** Maximum: 15 minutes
- **Aspect Ratio:** 1:1, 4:5, 9:16, or 16:9
- **Frame Rate:** Up to 30 FPS recommended

## Bluesky Video Requirements

- **File Size:** Maximum: 100 MB
- **Supported Formats:** MP4, MPEG, WebM, MOV
- **Duration:** Minimum: 1 second, Maximum: 3 minutes (180 seconds)
- **Frame Rate:** 10-60 FPS
- **Resolution:** Minimum: 360x360 px, Maximum: 1920x1920 px
- **Aspect Ratio:** Automatically detected and passed as metadata
- **Daily Limit:** 50 uploads per day (combined photos and videos)

## Discord Video Requirements

- **Supported Formats:** MP4, MOV, WebM (any video type Discord accepts)
- **Caption:** Up to 2,000 characters, applied as the message content
- **Max File Size:** Bounded by the upload limit of the target Discord server. The video is sent as a single message attachment.

## Telegram Video Requirements

- **Supported Formats:** MP4, MOV (any video type Telegram accepts)
- **Caption:** Up to 1,024 characters
- **Delivery:** Sent via `sendVideo` by your connected bot. File size is bounded by the Telegram Bot API upload limit.


---
# Webhooks & Notifications
URL: https://docs.upload-post.com/api/webhooks

Upload-Post allows you to receive real-time notifications about upload statuses and social account connection changes. This eliminates the need to poll endpoints for status updates.

## Configuration

You can configure notifications in the **Upload-Post Dashboard**:

[**Configure Notifications**](https://app.upload-post.com/notifications)

You can choose to receive notifications via:
- **Webhook**: A POST request sent to your server with a JSON payload.
- **Telegram**: A message sent to a configured Telegram chat.

### Configuration via API

You can also configure your notification preferences programmatically using the API.

**Endpoint:** `POST https://app.upload-post.com/api/uploadposts/users/notifications`

**Authentication:** Requires a valid API Key.

**Request Body:**

```json
{
  "channels": {
    "webhook": true,
    "telegram": false
  },
  "webhook_url": "https://your-server.com/webhook-endpoint",
  "telegram_chat_id": "123456789",
  "webhook_events": {
    "upload_completed": true,
    "social_account_connected": true,
    "social_account_disconnected": true,
    "social_account_reauth_required": true
  }
}
```

**Response:**

```json
{
  "success": true,
  "notifications": {
    "channels": { "webhook": true, ... },
    "webhook_url": "...",
    "webhook_events": { "upload_completed": true, "social_account_connected": true, ... },
    ...
  }
}
```

### Webhook Events

You can subscribe to specific event types using the `webhook_events` object. Set each event key to `true` to receive it, or `false` to disable it. If omitted, all events are enabled by default.

| Event | Description |
| :--- | :--- |
| `upload_completed` | Fired when an upload process completes (success or failure). |
| `social_account_connected` | Fired when a social account is connected or reconnected. |
| `social_account_disconnected` | Fired when a social account is disconnected (manually or automatically). |
| `social_account_reauth_required` | Fired when a social account requires re-authentication. |

## Webhook Payloads

### `upload_completed`

Sent when an upload process completes (whether successfully or with a failure).

```json
{
  "event": "upload_completed",
  "job_id": "a1b2c3d4e5f6...",
  "user_email": "user@example.com",
  "profile_username": "your_profile_username",
  "platform": "instagram",
  "media_type": "video",
  "title": "My Awesome Video",
  "caption": "Check this out! #cool",
  "result": {
    "success": true,
    "url": "https://www.instagram.com/p/C1234567890/",
    "publish_id": "17987654321098765",
    "post_id": "17987654321098765",
    "error": null
  },
  "created_at": "2024-03-15T14:30:00.000000"
}
```

#### Field Descriptions

| Field | Type | Description |
| :--- | :--- | :--- |
| `event` | `string` | The type of event: `upload_completed`. |
| `job_id` | `string` | The persistent job identifier returned when the post was created or scheduled. Use this to correlate webhook events with your original API requests. Only present when the upload was triggered via the API with a `job_id`. |
| `user_email` | `string` | The email address of the user who initiated the upload. |
| `profile_username` | `string` | The username of the profile associated with the upload. |
| `platform` | `string` | The social platform where the post was uploaded (e.g., `instagram`, `youtube`, `tiktok`). |
| `media_type` | `string` | The type of media uploaded (`video`, `photo`, or `text`). |
| `title` | `string` | The title provided for the post. |
| `caption` | `string` | The caption or description of the post. |
| `result` | `object` | An object containing the outcome of the upload attempt. |
| `result.success` | `boolean` | `true` if the upload was successful, `false` otherwise. |
| `result.url` | `string` | The direct URL to the published post (if available and successful). |
| `result.publish_id` | `string` | The ID assigned to the post by the platform. |
| `result.error` | `string` | A description of the error if the upload failed. |
| `created_at` | `string` | The timestamp of the event in ISO 8601 format. |

### `social_account_connected`

Sent when a social account is successfully connected or reconnected via OAuth.

```json
{
  "event": "social_account_connected",
  "user_email": "user@example.com",
  "platform": "instagram",
  "account_name": "my_instagram_handle",
  "status": "connected",
  "profile_username": "your_profile_username",
  "created_at": "2024-03-15T14:30:00.000000"
}
```

### `social_account_disconnected`

Sent when a social account is disconnected. This can happen due to:
- Manual disconnection by the user
- Automatic disconnection due to persistent authentication failures (account blocked)

```json
{
  "event": "social_account_disconnected",
  "user_email": "user@example.com",
  "platform": "tiktok",
  "account_name": "my_tiktok_handle",
  "status": "disconnected",
  "profile_username": "your_profile_username",
  "reason": "manual_disconnect",
  "created_at": "2024-03-15T14:30:00.000000"
}
```

### `social_account_reauth_required`

Sent when a social account's access token can no longer be refreshed and the user must re-authenticate. This typically happens when:
- The refresh token has expired
- The user revoked access on the platform
- Multiple consecutive token refresh attempts have failed

```json
{
  "event": "social_account_reauth_required",
  "user_email": "user@example.com",
  "platform": "youtube",
  "account_name": "UCxxxxxxxxx",
  "status": "reauth_required",
  "reason": "token_refresh_threshold_exceeded",
  "created_at": "2024-03-15T14:30:00.000000"
}
```

### Connection Status Event Fields

| Field | Type | Description |
| :--- | :--- | :--- |
| `event` | `string` | The event type: `social_account_connected`, `social_account_disconnected`, or `social_account_reauth_required`. |
| `user_email` | `string` | The email address of the account owner. |
| `platform` | `string` | The social platform (e.g., `instagram`, `youtube`, `tiktok`, `x`, `linkedin`, `facebook`, `threads`, `pinterest`, `reddit`, `bluesky`, `snapchat`, `google_business`, `tiktok_business`, `discord`, `telegram`, `slack`, `mastodon`, `nostr`, `lemmy`, `devto`, `hashnode`, `wordpress`, `whop`, `listmonk`). |
| `account_name` | `string` | The account identifier on the platform (e.g., username, channel ID). |
| `status` | `string` | The new connection status: `connected`, `disconnected`, or `reauth_required`. |
| `profile_username` | `string` | The Upload-Post profile associated with this account (if applicable). |
| `reason` | `string` | Additional context for the status change (e.g., `manual_disconnect`, `account_blocked`, `token_refresh_threshold_exceeded`, `max_auth_strikes`). Only present for `disconnected` and `reauth_required` events. |
| `created_at` | `string` | The timestamp of the event in ISO 8601 format. |

## Usage Notes

- **Idempotency**: While we strive to deliver each notification exactly once, you should handle potential duplicate events based on your own unique identifiers if necessary (though `post_id` or `publish_id` can serve this purpose for successful posts).
- **Security**: Ensure your webhook endpoint is secure (HTTPS) and verify the data as needed for your application logic.
- **Event Filtering**: Use the `webhook_events` field in your notification settings to subscribe only to the events you need. If not specified, all events are enabled by default.
- **Replacing Polling**: If you were previously polling the profile endpoint to check connection status, subscribe to `social_account_connected`, `social_account_disconnected`, and `social_account_reauth_required` events instead.


---
# AI Shorts Uploader
URL: https://docs.upload-post.com/guides/ai-shorts-uploader

# AI Shorts Uploader

The **AI Shorts Uploader** is a built-in assistant in the Upload-Post dashboard that watches your short-form video and writes the title, description and hashtags for each platform you publish to — **YouTube, Instagram and TikTok** — in seconds.

It is available from the **Shorts Uploader** screen in the app via the **Generate with AI** button.

## What it does

When you click **Generate with AI**, the platform:

1. Sends your video to a multimodal AI model (Gemini).
2. Analyses the visual content, pacing and on-screen text.
3. Returns optimized copy per platform:
   - **YouTube** — title (≤100 chars) + description (≤5,000 chars) with relevant hashtags.
   - **Instagram** — caption (≤2,200 chars) tuned for Reels.
   - **TikTok** — caption (≤150 chars) tuned for the For You feed.

You can also rewrite an already-generated set of captions into another language (Spanish, English, Japanese, …) without re-uploading the video. Each rewrite counts as one analysis.

## Monthly quotas per plan

Each successful analysis (and each language rewrite) consumes one unit from your monthly quota. The counter resets every 30 days from your last reset.

| Plan          | Analyses per month |
| :------------ | -----------------: |
| Free          |                 10 |
| Basic         |                100 |
| Professional  |                300 |
| Advanced      |                600 |
| Business      |              1,000 |

When you reach your quota, the dashboard shows an in-app message and the API responds with **HTTP 429**. Quotas reset automatically at the start of your next billing cycle.

## Video requirements

| Constraint   | Limit         |
| :----------- | :------------ |
| Max file size | 100 MB       |
| Max duration | 5 minutes     |
| Format       | `.mp4` recommended |

These limits exist because the feature is tuned for short-form vertical content. For longer videos, generate captions manually or use the [FFmpeg Video Editor API](../api/ffmpeg-editor.md) to trim a highlight first.

## Error responses

When the monthly quota is exhausted:

```json
{
  "success": false,
  "message": "You have used all 100 AI Shorts analyses for this month. Upgrade your plan for a higher quota or wait until your next billing period.",
  "remaining_analyses": 0
}
```

HTTP status: `429 Too Many Requests`.

If you also see a separate yellow banner reading *"Too many requests. Please wait a moment before trying again"*, that is the global per-minute API rate limit (see [Rate limits](./rate-limits.md)) — wait a few seconds and retry.

## FAQ

**Does retrying a failed analysis count twice?** No. The counter is incremented only after a successful analysis is returned to your browser.

**Can I see how many analyses I have left?** The dashboard shows the remaining count in the response of every analysis. A standalone usage endpoint is on the roadmap.

**Is the feature available on the public API?** Today it is dashboard-only. API access for `analyze-shorts` is on the roadmap; reach out to support if you need early access.


---
# Airtable Integration
URL: https://docs.upload-post.com/guides/airtable-integration

# Airtable Integration

Manage your video uploads to social media platforms directly from Airtable.

## Set Up

Running Airtable Automation Scripts requires a paid Airtable plan that includes automations with scripts.

This guide shows you how to automatically upload videos to social media platforms from Airtable via Upload-Post.

## Gather Your API Key

Start by getting your API Key from your [Upload-Post account](https://app.upload-post.com/) in the "API Keys" section. This key will be used in the script.

Be sure you have configured your social media accounts in Upload-Post before proceeding.

:::info
**URL Support for Media Files**: You can now pass URLs for both photo and video uploads instead of binary files. Simply provide the direct URL to your media file in the `video` or `photos[]` parameter.
:::

## Create an Airtable Workspace

In Airtable, create a new workspace with these fields:

* `Title` as Single Line Text
* `Platforms` as Multi Select with types: tiktok, instagram
* `Video` as Attachment
* `User` as Single Line Text
* `Status` as Single Line Text

## Enter Test Video Data

Add sample data to test the integration:

* `Title`: Enter a title for your video
* `Platforms`: Select one or more platforms (tiktok, instagram)
* `Video`: Attach a video file (MP4 format recommended)
* `User`: Enter your username
* `Status`: Enter `pending` (lowercase)

## Build an Automation Script

Let's create an Airtable automation script that uploads videos via Upload-Post:

### Add Trigger

1. In your workspace, click on _Automation_ then _+New automation_
2. Name the automation
3. Click _Choose a Trigger_
4. Select _When a Record is Created_
5. Select your table
6. Click Done

### Add Action

1. Click _Add Action_
2. Select _Run Script_
3. Delete any default code in the script editor
4. Copy and paste this code:

```javascript
const API_KEY = "Your API Key"; // Get your key at app.upload-post.com

console.log(`Starting Upload ${base.name}!`);

const uploadVideo = async (data) => {
  const { title, platforms, videoUrl, user } = data;
  
  const formData = new FormData();
  
  if (title) formData.append('title', title);
  if (user) formData.append('user', user);
  
  // Add platforms
  if (platforms && platforms.length > 0) {
    platforms.forEach(platform => {
      formData.append('platform[]', platform);
    });
  }
  
  // Download and attach video
  if (videoUrl) {
    const videoResponse = await fetch(videoUrl);
    const videoBlob = await videoResponse.blob();
    formData.append('video', videoBlob);
  }

  console.log("Uploading video to platforms:", platforms);

  const response = await fetch("https://api.upload-post.com/api/upload", {
    method: "POST",
    body: formData,
    headers: {
      "Authorization": `Apikey ${API_KEY}`
    }
  }).then((res) => res.json());

  return response;
};

const table = base.getTable("Videos");
const query = await table.selectRecordsAsync();
const filteredRecords = query.records.filter((record) => {
  const status = record.getCellValue("Status");
  return status === "pending";
});

for (let record of filteredRecords) {
  const title = record.getCellValue("Title");
  const video = record.getCellValue("Video");
  const platforms = record.getCellValue("Platforms");
  const user = record.getCellValue("User");

  if (!video || video.length === 0) {
    console.log("No video found for record");
    continue;
  }

  const response = await uploadVideo({
    title,
    platforms: platforms.map((x) => x.name),
    videoUrl: video[0].url,
    user
  });

  console.log(response);

  if (response) {
    let status = response.status === "success" ? "success" : "error";
    
    await table.updateRecordAsync(record, {
      Status: status
    });
  }
}
```

Replace `Your API Key` with your actual Upload-Post API key.

## Test the Script

In the script editor, press _>Test_

The script will run and process any pending records. If successful, you'll see your videos being uploaded to the selected platforms, and the Status field will update to "success".

## Security Best Practices

- Never share your API key
- Consider using environment variables where possible
- Review records before processing large batches


---
# Avoid Timeouts with Asynchronous Uploads
URL: https://docs.upload-post.com/guides/async-uploads

# Avoid Timeouts with Asynchronous Uploads

Are your requests taking too long and resulting in timeouts? For video, photo, or text post uploads that may require more processing time (file processing, social network publishing queues, etc.), use the `async_upload` parameter to make your request asynchronously.

## How does it work?

- Send your request with `async_upload=true` to the appropriate upload endpoint.
- The API will immediately respond with a `request_id`.
- Use this `request_id` to check the progress and result at the status endpoint.

## Checking Status

The status endpoint supports two different identifier types:

1. **`request_id`**: Returned by upload endpoints when `async_upload=true`
2. **`job_id`**: Returned when you schedule posts with `scheduled_date`

### For Async Uploads

```bash
GET /api/uploadposts/status?request_id=<REQUEST_ID>
```

### For Scheduled Posts

After scheduling a post with `scheduled_date`, the API returns a `job_id`. Use it to check the status after the scheduled time:

```bash
GET /api/uploadposts/status?job_id=<JOB_ID>
```

## Relevant Endpoints

- [Text upload](../api/upload-text): `POST /api/upload_text`
- [Video upload](../api/upload-video): `POST /api/upload`
- [Photo upload](../api/upload-photo): `POST /api/upload_photos`
- [Upload status](../api/upload-status): `GET /api/uploadposts/status?request_id=<REQUEST_ID>` or `?job_id=<JOB_ID>`

## Quick Example: Asynchronous Video Upload


---
# Authentication
URL: https://docs.upload-post.com/guides/authentication

# Authentication

Upload-Post uses API keys to authenticate requests. This guide explains how to obtain and use your API key.

## Getting Your API Key

1. Log in to your [Upload-Post Dashboard](https://app.upload-post.com/)
2. Navigate to the "API Keys" section
3. Click "Generate New API Key"
4. Copy and securely store your API key

## Using Your API Key

Include your API key in the `Authorization` header of all API requests:

```bash
Authorization: Apikey your-api-key-here
```

### Example Request

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="My Video"' \
  -F 'user="test"' \
  -F 'platform[]=tiktok' \
  -X POST https://api.upload-post.com/api/upload
```

## Security Best Practices

- **Never share your API key**: Keep your API key confidential
- **Use environment variables**: Store your API key in environment variables
- **Rotate keys regularly**: Generate new API keys periodically
- **Restrict access**: Only share API keys with trusted team members
- **Monitor usage**: Regularly check your API key usage in the dashboard

## API Key Limits

- Free tier includes 10 uploads per month
- Additional uploads available through paid plans

## Troubleshooting

If you receive a 401 Unauthorized error:
1. Verify your API key is correct
2. Check if your API key has expired
3. Ensure you're using the correct header format
4. Confirm your account is active

For additional help, contact our [support team](mailto:info@upload-post.com).


---
# Connecting Accounts
URL: https://docs.upload-post.com/guides/connecting-accounts

# Connecting Social Accounts

## Manual Credential Connections

Most platforms are connected through the JWT connection page (OAuth). A few platforms use a **manual credential** model instead: you submit the credentials to a dedicated endpoint, Upload-Post validates them with the platform, encrypts the secret at rest, and links it to the profile — **no OAuth and no browser redirect**. These credentials do not expire.

### Connecting Discord (manual credentials)

Discord is connected with a **channel incoming webhook URL**, not OAuth.

1. In Discord, open **Server Settings → Integrations → Webhooks → New Webhook**.
2. Choose the channel the posts should land in and click **Copy Webhook URL**.
3. Send the webhook URL to Upload-Post:

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/discord/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_username": "your_user_id_123",
    "webhook_url": "https://discord.com/api/webhooks/123456789/abcdef...",
    "name": "My Server #announcements"
  }'
```

Upload-Post validates the webhook (GET on the URL), encrypts and stores it, and sets `social_accounts.discord` on the profile. After connecting, post with `platform[]=discord` on the [text](../api/upload-text.md), [photo](../api/upload-photo.md), and [video](../api/upload-video.md) endpoints. Discord supports text (max 2000 chars), up to 10 images per message, and video. Analytics are not available for Discord.

➡️ **See details:** [Connect Discord API Reference](../api/user-profiles.md#connect-discord)

### Connecting Telegram (manual credentials)

Telegram is connected with **your own bot** plus a target chat, not OAuth.

1. In Telegram, message [@BotFather](https://t.me/BotFather), send `/newbot`, and follow the prompts to obtain a **bot token**.
2. Add the bot to the target channel or group **as an administrator** (it must be an admin to post).
3. Determine the `chat_id`: a public channel can use its `@username`; private chats use the numeric id (e.g. `-100123456789`).
4. Send the credentials to Upload-Post:

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/telegram/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_username": "your_user_id_123",
    "bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
    "chat_id": "@my_channel",
    "name": "My Channel"
  }'
```

Upload-Post validates the token (`getMe`) and chat (`getChat`), encrypts and stores the bot token, and sets `social_accounts.telegram` on the profile. After connecting, post with `platform[]=telegram` on the [text](../api/upload-text.md), [photo](../api/upload-photo.md), and [video](../api/upload-video.md) endpoints. Telegram supports text (max 4096 chars), single or multiple photos (album), and video (caption max 1024 chars). Analytics are not available for Telegram.

➡️ **See details:** [Connect Telegram API Reference](../api/user-profiles.md#connect-telegram)

### Connecting Slack (manual credentials)

Slack is connected with a channel **Incoming Webhook URL** (no OAuth). In Slack, create an app, enable *Incoming Webhooks*, and add one to the target channel to obtain a URL like `https://hooks.slack.com/services/T.../B.../xxxx`.

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/slack/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "webhook_url": "https://hooks.slack.com/services/T00/B00/xxxx", "name": "Team Channel" }'
```

After connecting, post with `platform[]=slack` on the [text](../api/upload-text.md) endpoint (text only; a Slack Incoming Webhook cannot upload media). Use `slack_title` to override the shared caption. No analytics.

### Connecting Mastodon (manual credentials)

Mastodon uses a per-instance **access token**. In your Mastodon instance go to *Preferences → Development → New application*, then copy the access token.

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/mastodon/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "instance_url": "https://mastodon.social", "access_token": "YOUR_TOKEN", "name": "@you@mastodon.social" }'
```

Supports `platform[]=mastodon` on text, photo (up to 4 images), and video endpoints. Use `mastodon_title` to override. No analytics.

### Connecting Nostr (manual credentials)

Nostr signs notes with **your private key** (nsec or hex) and broadcasts to relays. No server account is required.

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/nostr/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "private_key": "nsec1...", "relays": "wss://relay.damus.io,wss://nos.lol", "name": "My Nostr" }'
```

Text only (`platform[]=nostr`); `relays` is optional (sensible defaults are used). Use `nostr_title` to override. No analytics.

### Connecting Lemmy (manual credentials)

Lemmy uses your **instance URL, username, password**, and a target **community**. Posts require a community.

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/lemmy/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "instance_url": "https://lemmy.world", "username": "you", "password": "YOUR_PASSWORD", "community": "technology", "name": "Lemmy" }'
```

Supports `platform[]=lemmy` on the text and photo endpoints (video is not natively hosted). A title is required per post (`lemmy_title` or derived from the first line). No analytics.

### Connecting Dev.to (manual credentials)

Dev.to publishes Markdown articles with a single **API key** (Settings → Extensions → DEV API Keys).

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/devto/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "api_key": "YOUR_DEV_API_KEY", "name": "Dev.to" }'
```

Text/article only (`platform[]=devto`). `devto_title` sets the article title (or it is derived from the first line); the shared caption becomes the Markdown body. No analytics.

### Connecting Hashnode (manual credentials)

Hashnode uses a **Personal Access Token** and the target **publication ID**.

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/hashnode/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "pat": "YOUR_PAT", "publication_id": "PUBLICATION_ID", "name": "My Blog" }'
```

Text/article only (`platform[]=hashnode`). `hashnode_title` sets the post title; the caption becomes the Markdown content. No analytics.

### Connecting WordPress (manual credentials)

WordPress (self-hosted) uses your **site URL, username, and an Application Password** (Users → Profile → Application Passwords).

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/wordpress/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "site_url": "https://myblog.com", "username": "admin", "application_password": "xxxx xxxx xxxx xxxx", "name": "My Blog" }'
```

Supports `platform[]=wordpress` on text (posts), photo (featured image), and video endpoints. `wordpress_title` sets the post title; the caption becomes the post body. No analytics.

### Connecting Whop (manual credentials)

Whop posts to a community forum with an **API key** and the target **experience ID**.

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/whop/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "api_key": "YOUR_WHOP_API_KEY", "experience_id": "exp_XXXX", "name": "My Community" }'
```

Text only (`platform[]=whop`). Posting to a community forum requires an API key with the `forum:post:create` permission. Use `whop_title` to set the post title. No analytics.

### Connecting Listmonk (manual credentials)

Listmonk (self-hosted newsletter) uses your **instance URL, username, password**, and a target **list ID**. Each post creates and sends a campaign.

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/listmonk/credentials \
  -H "Authorization: Apikey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "profile_username": "your_user_id_123", "instance_url": "https://list.myblog.com", "username": "admin", "password": "YOUR_PASSWORD", "list_id": "1", "name": "Newsletter" }'
```

Text only (`platform[]=listmonk`). `listmonk_title` sets the campaign subject; the caption becomes the email body. No analytics.


---
# API Error Handling: Upload Endpoints
URL: https://docs.upload-post.com/guides/error-handling

# API Error Handling: Upload Endpoints

This document explains the structure of responses you will receive from the Video Upload (`POST /api/upload`) and Photo Upload (`POST /api/upload_photos`) endpoints, including how success and errors are indicated.

## 1. Successful Request Processing (HTTP 200 OK)

When your upload request is successfully processed by our server (meaning your authentication was valid, input was generally okay, and usage limits weren't exceeded *before* starting), you will **always receive an HTTP `200 OK` status code**.

The JSON response body will look like this:

```json
{
  "success": true,
  "results": {
    "tiktok": {
      "success": true,
      "publish_id": "1234567890123456789",
      "status": "PUBLISH_SUCCESS"
    },
    "instagram": {
      "success": true,
      "container_id": "9876543210987654321"
    },
    "linkedin": {
       "success": false,
       "error": "Your LinkedIn session has expired. Please reconnect your LinkedIn account. Go to Manage Users, remove the account, and connect it again."
    }
    // ... other requested platforms
  },
  "usage": {
    "count": 5,
    "limit": 120,
    "last_reset": "2023-10-27T10:00:00.000Z"
  }
}
```

**Key Points:**

*   `"success": true` indicates the API server processed your request.
*   **`"results"`:** This dictionary is crucial. It contains the outcome for **each individual platform** you requested.
    *   **Platform Success:** If `results[platform].success` is `true`, the upload to that platform likely succeeded. Additional platform-specific IDs (like `publish_id`, `container_id`, `post_id`) may be included.
    *   **Platform Failure:** If `results[platform].success` is `false`, the upload to that specific platform failed. The `results[platform].error` field will contain a message explaining the reason (e.g., token expired, API error from the platform, file issue).
    *   **Important:** An error on one platform (like LinkedIn in the example) **does not stop** attempts on other platforms. Always check the `success` flag for each platform in the `results`.
*   `"usage"`: Provides information about your current API usage count and limit after this request.

## 2. Request Failure (Non-200 HTTP Status Codes)

If there's a fundamental problem with your request *before* we attempt to upload to individual platforms, you will receive an HTTP status code other than `200 OK`. The response body will typically look like this:

```json
{
  "success": false,
  "message": "A description of the error"
}
```

Here are common error status codes and their meanings:

*   **`400 Bad Request`**
    *   **Meaning:** Your request was malformed or missing required information.
    *   **Common Causes:** Missing `video` or `photos` file, missing `title`, invalid `platform` name, missing `user` identifier in the form data when required.
    *   **`message` examples:** `"Video file and title are required"`, `"Title cannot be empty"`, `"Username required"`, `"Invalid platforms: [platform_name]"`, `"Username not associated with any profile"`.

*   **`401 Unauthorized`**
    *   **Meaning:** Authentication failed.
    *   **Common Causes:** Missing `Authorization` header, using an invalid or expired API Key or Bearer Token.
    *   **`message` examples:** `"Authorization header required"`, `"Invalid or expired token"`, `"Invalid API key"`, `"API key expired"`.

*   **`404 Not Found`**
    *   **Meaning:** The user associated with your authentication could not be found in our system.
    *   **`message` example:** `"User not found"`.

*   **`429 Too Many Requests`**
    *   **Meaning:** You have exceeded an API usage limit.
    *   **Common Causes:** Reaching your monthly upload limit, or (for Professional plan users) reaching the daily upload limit for a specific social media account.
    *   **`message` examples:** `"You have reached your monthly limit of X uploads"`, `"You have reached the daily limit of 5 uploads for: Instagram (account_name)"`.

*   **`500 Internal Server Error`**
    *   **Meaning:** An unexpected error occurred on our server while processing your request.
    *   **`message` example:** Usually contains technical details about the error. If you encounter this repeatedly, please contact support.

**In summary:** Always check the HTTP status code first. If it's `200 OK`, examine the `success` flag within the `results` dictionary for each platform. If it's not `200 OK`, check the `message` field in the response body for the reason.


---
# Limit of uploads
URL: https://docs.upload-post.com/guides/limit-of-uploads

# Limit of uploads

### Social Hard Caps Per Network

To protect your connected accounts and stay compliant with each social network, Upload-Post enforces platform hard caps using a rolling 24-hour window. When a cap is reached for a specific account on a given network, further posts to that account/network are rejected until the window rolls over.

- What counts toward the cap: only successful publishes recorded for that account/network in the last 24 hours.
- Scope: **Per connected social account**. These limits are NOT global to your Upload-Post user.
  - **Example**: If you manage **5 Profiles**, and each Profile has its own TikTok account connected, you get the full limit for *each* TikTok account. (e.g., 5 TikTok accounts × 15 posts = **75 posts per day** total).
- Scheduled posts: caps are re-checked at execution time; if the cap is already reached, the publish will be rejected then.

Recommended and enforced daily caps

| Social Network     | Hard Cap (posts per 24h) |
| :----------------- | -----------------------: |
| Instagram          |                       50 |
| TikTok             |                       15 |
| LinkedIn           |                      150 |
| YouTube            |                       10 |
| Facebook           |                       25 |
| X (Twitter)        |              See per-plan table below |
| Threads            |                       50 |
| Pinterest          |                       20 |
| Reddit             |                       40 |
| Bluesky            |                       50 |

#### X (Twitter) per-plan daily caps

X uploads have a per-plan hard cap (one cap per connected X profile, per 24-hour rolling window) instead of a single flat limit. All other platforms keep the flat caps shown above.

| Plan         | X Hard Cap (posts per profile per 24h) |
| :----------- | -------------------------------------: |
| Default (free) |                                   10 |
| Basic        |                                     10 |
| Professional |                                     20 |
| Advanced     |                                     30 |
| Business     |                                     30 |

Error response when the cap is reached

- Status: 429 Too Many Requests
- Body example:
```json
{
  "success": false,
  "message": "Post verification failed",
  "violations": [
    {
      "platform": "instagram",
      "type": "hard_cap",
      "message": "Daily cap reached for instagram: 50/50 in last 24h",
      "used_last_24h": 50,
      "cap": 50
    }
  ]
}
```

What else the verifier checks

- Duplicate/similar content within 48h (per account/network) to reduce spam risk and shadow bans.
- Mention limits to avoid spammy behavior (e.g., excessive mentions or repeating the same handle too frequently).
- Media and content sanity checks evolve over time to align with network guidelines.


---
# Make Integration
URL: https://docs.upload-post.com/guides/make-integration

# Make Integration

Upload-Post provides seamless integration with Make (formerly Integromat) for automated video publishing workflows. This guide walks you through connecting your Upload-Post account with Make in 3 simple steps.

## Getting Started with Upload-Post

1. Create an account or log in to your existing [Upload-Post account](https://app.upload-post.com/)
2. Navigate to the "API Keys" section
3. Generate an API key for your Make integration

## API Configuration

For Make integration, you'll need to configure an HTTP module with the following parameters:

```bash
Endpoint: https://api.upload-post.com/api/upload
Method: POST
Headers: Authorization: Apikey your-api-key-here
```

**Note:** Find your API key in your [Upload-Post Manage API Keys](https://app.upload-post.com/) section.

:::info
**URL Support for Media Files**: You can now pass URLs for both photo and video uploads instead of binary files. Simply provide the direct URL to your media file in the `video` or `photos[]` parameter.
:::

## Form Data Configuration & Make.com Setup

Configure your Make HTTP module with these parameters:

| Field | Value | Required |
| ----- | ----- | -------- |
| title | Your video title | Optional |
| user | Your username | Required |
| platform[] | tiktok | Required |
| video | Binary file | Required |

### Make.com Configuration Steps:

1. Add an **HTTP Module**: In your Make.com scenario, add an HTTP module and choose the "Make a Request" action.
2. Configure the Request Settings:
   - **Method**: Set to POST.
   - **URL**: Enter `https://api.upload-post.com/api/upload`.
   - **Headers**: Add a header with:
     - **Key**: Authorization
     - **Value**: `Apikey [YOUR_API_KEY]`
   - Set the **Request Body**: Change the body type to `multipart/form-data` and add the following form fields:
     - **title**: Set the value to your desired title (you can use a variable if needed).
     - **user**: Enter your username you set in Upload-Post.
     - **platform[]**: Set the value to `tiktok`.
     - **video**: Attach the binary file (your video file). Make sure this field is mapped to the binary data you want to send.
3. Save and Test: Save your scenario and run a test to ensure that the video upload works correctly via the API.

## Advanced Configuration Options

### For Instagram Uploads

To upload to Instagram instead, simply change the platform value to `instagram` in your form data.

### Uploading to Multiple Platforms

To upload to both TikTok and Instagram simultaneously, add both platform values by creating multiple fields with the same name `platform[]` in the Make.com HTTP module.

### Securely Storing API Keys in Make.com

For better security, avoid hardcoding your API key directly in scenarios:

- Create an **App Key** in Make.com
- Store your Upload-Post API key as a constant
- Reference the constant in your HTTP module headers
- When sharing scenarios, use scenario blueprints which do not expose your keys

Example of referencing an API key constant in Make:

```json
"headers": {
  "Authorization": "Apikey {{constants.uploadPostApiKey}}"
}
```

Need more guidance? Check out this detailed forum post: [Make.com Community Tutorial](https://community.make.com/t/how-to-upload-video-to-tiktok-and-instagram/69264)

## Need Assistance?

For additional help with your Make integration, contact our [support team](mailto:info@upload-post.com).


---
# MCP Server (ChatGPT / Claude / Cursor)
URL: https://docs.upload-post.com/guides/mcp-server-integration

# MCP Server (ChatGPT / Claude / Cursor)

Upload-Post ships an official **Model Context Protocol (MCP)** server. Connect it to ChatGPT, claude.ai, Claude Desktop, Claude Code, Cursor, or any other MCP-compatible AI agent and your assistant can publish, schedule, analyze and manage social media on your behalf — without writing any code.

The server exposes **50 tools** across the public Upload-Post API, OAuth connectors, media staging, and the hosted Upload Studio.

- **Hosted endpoint:** `https://mcp.upload-post.com/mcp`
- **Source:** [github.com/Upload-Post/upload-post-mcp](https://github.com/Upload-Post/upload-post-mcp) (MIT)

## How authentication works

The server supports **two auth modes**, and chooses automatically based on what the client sends:

### 1. API key (Claude Desktop, Claude Code, Cursor, any MCP client with `mcp.json`)

Each MCP client sends its own Upload-Post API key in the `Authorization` header of every request. The server uses that key for the duration of the session and stores nothing.

```
Authorization: ApiKey YOUR_UPLOAD_POST_API_KEY
```

`Authorization: Bearer YOUR_UPLOAD_POST_API_KEY` is also accepted, for clients that only allow Bearer tokens.

### 2. OAuth 2.1 (claude.ai Custom Connectors, ChatGPT, any "Add via URL" client)

Clients that don't accept custom headers can connect via the standard OAuth flow — no API key copying required. The server implements:

- **RFC 9728** Protected Resource Metadata at `/.well-known/oauth-protected-resource`
- **RFC 8414** Authorization Server Metadata at `/.well-known/oauth-authorization-server`
- **RFC 7591** Dynamic Client Registration at `/register`
- **RFC 6749 §4.1 + PKCE (RFC 7636)** authorization-code flow at `/authorize` + `/token`
- **RFC 7009** Token revocation at `/revoke`

The user is sent to `app.upload-post.com/oauth/authorize` to log in and approve. The issued access token (prefix `up_oauth_`) is opaque — the server resolves it to the user's API key on every request via an internal introspection call.

You can view and revoke active OAuth connectors at any time from **Connected Apps** in the dashboard.

## Get your API key

1. Sign in to [app.upload-post.com](https://app.upload-post.com/).
2. Open the **API Keys** section.
3. Generate a new key and copy it.

## Connect claude.ai (Custom Connector)

Go to **Settings → Connectors → Add custom connector** in claude.ai and paste:

```
https://mcp.upload-post.com/mcp
```

claude.ai will:

1. Discover the OAuth metadata at `/.well-known/oauth-protected-resource`.
2. Dynamically register itself via `/register` (no manual client ID needed).
3. Open `app.upload-post.com/oauth/authorize` in a popup so you can log in and approve.
4. Exchange the returned authorization code for an access token, and store it.

That's it — 50 tools become available inside claude.ai with zero local configuration. Revoke any time from **Connected Apps**.

## Connect ChatGPT

Use the ChatGPT connector/app setup and point it to the hosted MCP endpoint:

```
https://mcp.upload-post.com/mcp
```

ChatGPT uses the same OAuth flow as claude.ai. After approval, the connector can call the Upload-Post tools and open the Upload Studio component for browser-based media uploads.

## Connect Claude Desktop / Claude Code / Cursor

Add the following entry to your MCP config (`~/.claude/mcp.json`, `~/.cursor/mcp.json`, or your IDE's equivalent):

```json
{
  "mcpServers": {
    "upload-post": {
      "url": "https://mcp.upload-post.com/mcp",
      "headers": {
        "Authorization": "ApiKey YOUR_UPLOAD_POST_API_KEY"
      }
    }
  }
}
```

Restart your client. You should see 50 `upload-post` tools become available.

## What the agent can do

| Group         | Tools |
|---------------|-------|
| Upload        | `upload_video`, `upload_photos`, `upload_text`, `upload_document` |
| Status        | `get_status`, `get_job_status`, `get_history`, `get_media` |
| Schedule      | `list_scheduled`, `cancel_scheduled`, `edit_scheduled` |
| Analytics     | `get_analytics`, `get_total_impressions`, `get_post_analytics`, `get_platform_metrics` |
| Users         | `get_account_info`, `list_users`, `create_user`, `delete_user`, `generate_jwt`, `validate_jwt` |
| Pages / boards| `get_facebook_pages`, `get_linkedin_pages`, `get_pinterest_boards`, `get_google_business_locations`, `select_google_business_location`, `get_reddit_detailed_posts` |
| Comments      | `get_post_comments`, `reply_to_comment`, `public_reply_to_comment` |
| DMs           | `send_dm`, `list_dm_conversations`, `manage_autodms` |
| FFmpeg        | `submit_ffmpeg_job`, `get_ffmpeg_job`, `download_ffmpeg_result`, `get_ffmpeg_consumption` |
| Queue         | `get_queue_settings`, `update_queue_settings`, `preview_queue` |
| Media staging | `create_media_upload`, `complete_media_upload`, `get_media_upload`, `delete_media_upload` |
| Studio        | `open_upload_studio` |

Async uploads return a `request_id`; the agent polls `get_status` until `success: true`.

## Upload tool parameters

The four upload tools accept the same options as the REST API, but in **camelCase** (the MCP is built on the official [`upload-post` npm SDK](https://www.npmjs.com/package/upload-post), which maps them to the snake_case form fields of [`POST /api/upload`](../api/upload-video.md), [`POST /api/upload_photos`](../api/upload-photo.md) and [`POST /api/upload_text`](../api/upload-text.md)).

### Common parameters (all upload tools)

| Parameter | Type | Description |
|-----------|------|-------------|
| `user` | string | **Required.** Profile name (Upload-Post user). |
| `platforms` | string[] | **Required.** Array of platform identifiers, e.g. `["youtube", "tiktok"]`. |
| `title` | string | Caption / title. Required for YouTube, Reddit and text posts. |
| `description` | string | Description (platforms that support it). |
| `firstComment` | string | Comment posted right after publishing. |
| `scheduledDate` | string | ISO 8601 date for scheduled publishing, e.g. `2026-12-25T10:00:00Z`. Omit to post now. |
| `timezone` | string | IANA timezone for `scheduledDate`, e.g. `Europe/Madrid`. |
| `addToQueue` | boolean | Insert into the profile's [posting queue](../api/queue-system.md) instead of publishing now. |
| `asyncUpload` | boolean | Return immediately with a `request_id` (default `true`). |

Tool-specific media inputs:

- **`upload_video`** — `videoPathOrUrl` (public/signed HTTPS URL, or absolute local path for self-hosted MCP) **or** `videoBase64` (+ optional `videoFilename`). Hosted clients (ChatGPT, claude.ai) must use `open_upload_studio` / media staging instead of local paths.
- **`upload_photos`** — `photosPathsOrUrls` (array of URLs or local paths), optional `altText`.
- **`upload_text`** — `title` is the post text; optional `linkUrl` for a link-preview card (LinkedIn, Bluesky, Facebook).
- **`upload_document`** — `documentPathOrUrl` (PDF/PPT/PPTX/DOC/DOCX, LinkedIn only), `linkedinVisibility`, `targetLinkedinPageId`.

### `platformOptions` — per-platform overrides

`upload_video`, `upload_photos` and `upload_text` accept a flat `platformOptions` object with camelCase keys. Each key maps 1:1 to a snake_case parameter of the REST API (`youtubeThumbnailUrl` → `thumbnail_url`, `tiktokPrivacyLevel` → `privacy_level`, …); the supported set is the option list of the [`upload-post` SDK](https://www.npmjs.com/package/upload-post). The most used keys:

**YouTube** (video)

| Key | Description |
|-----|-------------|
| `youtubeThumbnailUrl` | Custom thumbnail image URL. |
| `youtubeTags` | Video tags — string or array of strings. |
| `youtubeCategoryId` | Category ID (e.g. `"22"` for People & Blogs). |
| `youtubePrivacyStatus` | `public`, `unlisted` or `private`. |
| `youtubePlaylistId` | One playlist ID, an array, or a comma-separated list to add the video to. |
| `youtubeEmbeddable` | Allow embedding on other sites. |
| `youtubeLicense` | `youtube` or `creativeCommon`. |
| `youtubePublicStatsViewable` | Show public view stats. |
| `youtubeSelfDeclaredMadeForKids` | COPPA made-for-kids flag. |
| `youtubeContainsSyntheticMedia` | AI/synthetic content disclosure. |
| `youtubeDefaultLanguage` / `youtubeDefaultAudioLanguage` | BCP-47 language of title/description and audio. |
| `youtubeAllowedCountries` / `youtubeBlockedCountries` | Comma-separated country codes. |
| `youtubeHasPaidProductPlacement` | Paid product placement flag. |
| `youtubeRecordingDate` | Recording date (ISO 8601). |
| `youtubeSubtitles` | Array of `{ language, name?, url? }` subtitle tracks (SRT, VTT, SBV, SUB, ASS, SSA, TTML). |

**TikTok**

| Key | Description |
|-----|-------------|
| `tiktokPrivacyLevel` | `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY`. |
| `tiktokDisableDuet` / `tiktokDisableComment` / `tiktokDisableStitch` | Interaction toggles. |
| `tiktokCoverTimestamp` | Cover frame timestamp in ms (video). |
| `tiktokPhotoCoverIndex` | Cover photo index, 0-based (photos). |
| `tiktokAutoAddMusic` | Auto add music (photos). |
| `tiktokIsAigc` | AI-generated content flag. |
| `tiktokPostMode` | `DIRECT_POST` or `MEDIA_UPLOAD`. |
| `brandContentToggle` / `brandOrganicToggle` | Branded content disclosures. |

**Instagram**

| Key | Description |
|-----|-------------|
| `instagramMediaType` | Video: `REELS` or `STORIES`. Photos: `IMAGE` or `STORIES`. |
| `instagramCoverUrl` | Custom cover image URL (Reels). |
| `instagramThumbOffset` | Frame offset for the auto thumbnail. |
| `instagramShareToFeed` | Also show the Reel in the feed. |
| `instagramCollaborators` | Comma-separated collaborator usernames. |
| `instagramUserTags` | Comma-separated user tags. |
| `instagramLocationId` | Location ID. |
| `instagramAudioName` | Audio track name. |

**Facebook**

| Key | Description |
|-----|-------------|
| `facebookPageId` | Page ID to publish to (see `get_facebook_pages`). |
| `facebookMediaType` | `REELS`, `STORIES` or `VIDEO`. |
| `facebookVideoState` | `PUBLISHED` or `DRAFT`. |
| `thumbnailUrl` | Thumbnail URL for normal page videos (`facebookMediaType: "VIDEO"`). |
| `facebookLinkUrl` | Link preview URL (text posts). |

**LinkedIn**

| Key | Description |
|-----|-------------|
| `linkedinPageId` / `targetLinkedinPageId` | Organization page ID (see `get_linkedin_pages`). |
| `linkedinVisibility` | `PUBLIC`, `CONNECTIONS`, `LOGGED_IN`, `CONTAINER`. |
| `linkedinLinkUrl` | Link preview URL (text posts). |

**Pinterest**

| Key | Description |
|-----|-------------|
| `pinterestBoardId` | Board ID (see `get_pinterest_boards`). |
| `pinterestLink` | Destination link for the pin. |
| `pinterestCoverImageUrl` | Cover image URL (video pins). |
| `pinterestCoverImageKeyFrameTime` | Key frame time in ms for the cover. |
| `pinterestAltText` | Alt text (photo pins). |

**X (Twitter)**

| Key | Description |
|-----|-------------|
| `xReplySettings` | Who can reply: `everyone`, `following`, `mentionedUsers`, `subscribers`, `verified`. |
| `xTaggedUserIds` | User IDs to tag in media. |
| `xLongTextAsPost` | Post long text as a single post instead of a thread. |
| `xThreadImageLayout` | Images per thread post, e.g. `"4,4"`. |
| `xQuoteTweetId` | Tweet ID to quote (text posts). |
| `xPollOptions` / `xPollDuration` | Poll with 2–4 options and duration in minutes (text posts). |
| `xCommunityId` | Community ID. |

**Threads / Reddit / Bluesky / Google Business**

| Key | Description |
|-----|-------------|
| `threadsLongTextAsPost` | Single post instead of a thread. |
| `threadsThreadMediaLayout` | Media items per Threads post, e.g. `"5,5"`. |
| `redditSubreddit` | Subreddit name (without `r/`). Title required. |
| `redditFlairId` | Flair template ID. |
| `blueskyLinkUrl` | External embed link preview. |
| `googleBusinessLocationId` | Location to publish to (see `get_google_business_locations`). |

Per-platform text overrides are also supported: `youtubeTitle`, `tiktokTitle`, `instagramTitle`, …, `youtubeDescription`, `linkedinDescription`, …, and `youtubeFirstComment`, `instagramFirstComment`, etc. take priority over the generic `title` / `description` / `firstComment` for that platform.

### Example: video to YouTube with thumbnail, tags and playlist

```json
{
  "tool": "upload_video",
  "arguments": {
    "user": "marketing",
    "platforms": ["youtube"],
    "title": "Spring launch 🌱",
    "description": "Everything new this season.",
    "videoPathOrUrl": "https://example.com/launch.mp4",
    "platformOptions": {
      "youtubePrivacyStatus": "public",
      "youtubeThumbnailUrl": "https://example.com/thumb.jpg",
      "youtubeTags": ["launch", "product"],
      "youtubeCategoryId": "22",
      "youtubePlaylistId": "PLxxxxxxxxxxxx"
    }
  }
}
```

For the complete parameter semantics, validation rules and platform requirements, see the REST references: [Upload Video](../api/upload-video.md), [Upload Photos](../api/upload-photo.md), [Upload Text](../api/upload-text.md) and [Upload Document](../api/upload-document.md).

## Browser media staging for ChatGPT and Claude

Hosted AI clients do not reliably hand local file paths to MCP tools. Upload-Post solves this with short-lived R2 media staging, so the browser uploads the binary directly while the MCP only orchestrates the flow.

1. The client calls `create_media_upload` with `filename`, `content_type`, `content_length`, `media_type`, and `source`.
2. The backend returns an `upload_id` and a presigned R2 `upload_url`.
3. The browser uploads the file directly to R2 with `PUT`.
4. The client calls `complete_media_upload` to validate the object and receive a temporary signed `media_url`.
5. The agent passes that `media_url` to `upload_video`, `upload_photos`, or another upload tool.

The staging object is not the durable source of truth. The normal Upload-Post upload/scheduler flow downloads the temporary URL and stores the media in the existing durable storage path before publishing or scheduling. This keeps the existing `/api/upload` behavior unchanged.

Staged objects expire after 24 hours whether they were used or not. Clients can also call `delete_media_upload` to remove an unused object earlier.

### Media staging limits

These limits are separate from normal social upload quotas and protect only the temporary browser-to-R2 ingest path.

| Plan | Uploads / month | Max file size | Monthly ingest | Pending uploads |
|------|-----------------|---------------|----------------|-----------------|
| Free / default | 10 | 250 MB | 2 GB | 3 |
| Basic / Premium | 100 | 500 MB | 25 GB | 10 |
| Professional / Pro | 500 | 1 GB | 100 GB | 50 |
| Advanced | 2,000 | 2 GB | 500 GB | 200 |
| Business | 10,000 | 5 GB | 2 TB | 1,000 |

## Production deployment notes

Deploy the backend before the MCP server. The MCP calls the new media staging endpoints, so older backend versions will not support browser uploads.

Backend environment variables are optional because production-safe defaults are built in:

```env
UPLOADPOST_MCP_MEDIA_STAGING_PREFIX=mcp-ingest/tmp
UPLOADPOST_MCP_MEDIA_TTL_HOURS=24
UPLOADPOST_MCP_MEDIA_PUT_EXPIRES_SECONDS=900
UPLOADPOST_MCP_MEDIA_GET_EXPIRES_SECONDS=21600
```

Set `UPLOAD_POST_R2_CONNECT_DOMAIN` on the MCP only if the public host used by the presigned R2 URLs is different from the default:

```env
UPLOAD_POST_R2_CONNECT_DOMAIN=https://0de16d5f5e344fe4757ecd62640a9ea3.r2.cloudflarestorage.com
```

R2 bucket CORS is required only for browser direct uploads. It does not affect API-to-API calls, scheduler workers, or the existing upload flow. Allow:

- Origins: `https://chatgpt.com`, `https://chat.openai.com`, `https://app.upload-post.com`, `https://mcp.upload-post.com`
- Methods: `PUT`, `GET`, `HEAD`
- Headers: at least `Content-Type`; use `*` if the Cloudflare dashboard requires a broad value for presigned S3 requests

Do not add a 24-hour lifecycle rule to the whole bucket. Cleanup is handled by the backend cron and targets only objects under `mcp-ingest/tmp`.

### Troubleshooting

- `redirect_uri not on allow-list`: the Upload-Post OAuth backend must allow the client redirect URI from ChatGPT or claude.ai before `/oauth/authorize` can complete.
- Browser `CORS` error during `PUT`: check the R2 CORS policy and make sure the MCP `UPLOAD_POST_R2_CONNECT_DOMAIN` matches the host returned in `upload_url`.
- `Video file not found` from an agent: use media staging instead of sending local filesystem paths to `upload_video`.

## Example prompts

Once connected, try these in your AI client:

- *"List the users in my Upload-Post account."*
- *"Publish this video URL to TikTok and Instagram under the profile `marketing` with the caption 'Spring launch'."*
- *"Schedule a text post on LinkedIn for next Monday at 10:00 Madrid time."*
- *"Show me the analytics for my profile `marketing` over the last month."*
- *"Reply privately to the latest comment on my Instagram post."*

The model decides which tools to call based on the request; you don't have to name them.

## Self-hosting (optional)

If you prefer to run the server yourself — for an isolated network, custom auth proxy, or stricter compliance requirements — the source repository ships with a multi-stage `Dockerfile` and a one-click Coolify configuration. See the project [README](https://github.com/Upload-Post/upload-post-mcp#deploy-on-coolify-docker) for instructions.

## Local-only stdio mode

For single-user setups (no hosted server), you can run the MCP locally via `npx`. Once published to npm, the configuration becomes:

```json
{
  "mcpServers": {
    "upload-post": {
      "command": "npx",
      "args": ["-y", "@upload-post/mcp"],
      "env": { "UPLOAD_POST_API_KEY": "YOUR_UPLOAD_POST_API_KEY" }
    }
  }
}
```

Both modes expose the same 50 tools.

## Need assistance?

Open an issue at [github.com/Upload-Post/upload-post-mcp/issues](https://github.com/Upload-Post/upload-post-mcp/issues) or contact our [support team](mailto:info@upload-post.com).


---
# n8n Integration
URL: https://docs.upload-post.com/guides/n8n-integration

# n8n Integration

Upload-Post provides seamless integration with n8n for automated video publishing workflows. This guide walks you through connecting your Upload-Post account with n8n.

## Getting Started with Upload-Post

1. Create an account or log in to your existing [Upload-Post account](https://app.upload-post.com/)
2. Navigate to the "API Keys" section
3. Generate an API key for your n8n integration

## API Configuration

For n8n integration, you'll need to configure an HTTP Request node with the following parameters:

```bash
Endpoint: https://api.upload-post.com/api/upload
Method: POST
Headers: Authorization: Apikey your-api-key-here
```

:::info
**URL Support for Media Files**: You can now pass URLs for both photo and video uploads instead of binary files. Simply provide the direct URL to your media file in the `video` or `photos[]` parameter.
:::

## n8n Workflow Configuration

Configure your n8n HTTP Request node with these parameters:

| Field | Value | Required |
| ----- | ----- | -------- |
| title | Your video title | Optional |
| user | Your username | Required |
| platform[] | tiktok | Required |
| video | Binary file | Required |

### Node Configuration Steps:

1. Add an **HTTP Request Node** to your workflow
2. Configure the node settings:
   - **Method**: POST
   - **URL**: `https://api.upload-post.com/api/upload`
   - **Headers**: `Authorization: Apikey [YOUR_API_KEY]`
   - **Body**: Set to `multipart/form-data` and add the required fields

## Complete JSON Node Configuration

Below is the complete JSON configuration for the HTTP Request node in n8n:

```json
{
  "parameters": {
    "method": "POST",
    "url": "https://api.upload-post.com/api/upload",
    "authentication": "none",
    "sendHeaders": true,
    "headerParameters": {
      "parameters": [
        {
          "name": "Authorization",
          "value": "Apikey YOUR_API_KEY_HERE"
        }
      ]
    },
    "sendBody": true,
    "bodyParameters": {
      "parameters": [
        {
          "name": "user",
          "value": "YOUR_USERNAME"
        },
        {
          "name": "title",
          "value": "= $input.item.title ? $input.item.title : 'My awesome video'"
        },
        {
          "name": "platform[]",
          "value": "tiktok"
        }
      ]
    },
    "options": {
      "redirect": {
        "redirect": true
      },
      "proxy": {
        "proxy": false
      },
      "timeout": 10000
    },
    "sendQuery": false,
    "contentType": "multipart-form-data",
    "queryParameterArrays": "indices",
    "bodyContentType": "multipart-form-data",
    "bodyParameterArrays": "indices",
    "formBinaryData": {
      "video": "={{$binary.data}}"
    }
  },
  "name": "Upload Video to TikTok",
  "type": "n8n-nodes-base.httpRequest",
  "typeVersion": 4,
  "position": [
    860,
    300
  ],
  "id": "98a25a44-b7fb-41e3-8b8b-3d33c5c6ea65"
}
```

### For Instagram Uploads

To upload to Instagram instead, change the platform value:

```json
"bodyParameters": {
  "parameters": [
    {
      "name": "user",
      "value": "YOUR_USERNAME"
    },
    {
      "name": "title",
      "value": "= $input.item.title ? $input.item.title : 'My awesome video'"
    },
    {
      "name": "platform[]",
      "value": "instagram"
    }
  ]
}
```

### Uploading to Multiple Platforms

To upload to both TikTok and Instagram simultaneously:

```json
"bodyParameters": {
  "parameters": [
    {
      "name": "user",
      "value": "YOUR_USERNAME"
    },
    {
      "name": "title",
      "value": "= $input.item.title ? $input.item.title : 'My awesome video'"
    },
    {
      "name": "platform[]",
      "value": "tiktok"
    },
    {
      "name": "platform[]",
      "value": "instagram"
    }
  ]
}
```

## Security Best Practices

- **Never hardcode your API key** directly in the workflow
- Create a **Credentials** entry in n8n for your Upload-Post API key
- Reference the credential in your HTTP Request node
- For workflows that will be shared, export without credentials
- Consider using environment variables or n8n's credential store

## Example Workflow: AI-powered Social Media Publisher

This workflow automates video publishing with AI-generated descriptions:

1. **Google Drive Trigger**: Monitors a folder for new videos
2. **OpenAI Transcription**: Extracts audio and converts to text
3. **OpenAI Description Generator**: Creates engaging descriptions
4. **Upload-Post HTTP Request**: Uploads to multiple platforms
5. **Error Handling**: Sends notifications on completion/errors

This workflow is available as a template: [View template on n8n.io](https://n8n.io/workflows/2894-upload-to-instagram-tiktok-and-youtube-from-google-drive/)

## Need Assistance?

For additional help with your n8n integration, contact our [support team](mailto:info@upload-post.com).


---
# How to Post to Instagram with an API
URL: https://docs.upload-post.com/guides/post-to-instagram-api

# How do I post to Instagram with an API, without my own Meta app?

Send a `multipart/form-data` request to Upload-Post's `POST https://api.upload-post.com/api/upload` (videos/Reels) or `POST /api/upload_photos` (photos and carousels) with `platform[]=instagram`. Upload-Post runs its own approved Meta app with the required Instagram permissions, so you skip the whole Meta developer flow: no app registration, no App Review, no permission approvals, no token refresh logic. You connect the Instagram account once via OAuth in the dashboard. It does have to be a **Business or Creator** account linked to a Facebook Page (that's an Instagram platform requirement).

## Steps

1. Create an account at [upload-post.com](https://www.upload-post.com) and generate an API key under **API Keys** ([Authentication](./authentication.md)).
2. Connect the Instagram account at [Manage Users](https://app.upload-post.com/manage-users), approving **all** requested permissions. To let *your* users connect their own Instagram accounts inside your product, generate a [white-label JWT connect link](./user-profile-integration.md) instead.
3. Post a Reel:

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="Your Reel caption"' \
  -F 'user="test"' \
  -F 'platform[]=instagram' \
  -X POST https://api.upload-post.com/api/upload
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload",
    headers={"Authorization": "Apikey your-api-key-here"},
    files={"video": open("/path/to/your/video.mp4", "rb")},
    data={
        "title": "Your Reel caption",
        "user": "test",
        "platform[]": "instagram",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript

const form = new FormData();
form.append("video", new Blob([fs.readFileSync("/path/to/your/video.mp4")]), "video.mp4");
form.append("title", "Your Reel caption");
form.append("user", "test");
form.append("platform[]", "instagram");

const response = await fetch("https://api.upload-post.com/api/upload", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

## Photos and carousels

Use [`POST /api/upload_photos`](../api/upload-photo.md) with a `photos[]` array (up to 10 items; Instagram also supports **mixed** photo + video carousels):

```bash
curl -X POST https://api.upload-post.com/api/upload_photos \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'photos[]=@image1.jpg' \
  -F 'photos[]=@image2.jpg' \
  -F 'photos[]=@video.mp4' \
  -F 'title="My mixed carousel"' \
  -F 'user="test"' \
  -F 'platform[]=instagram'
```

## Useful Instagram parameters

Full list in the [Upload Video reference](../api/upload-video.md#instagram):

| Parameter | What it does | Default |
|-----------|--------------|---------|
| `instagram_title` | Instagram-specific caption (falls back to `title`) | `title` |
| `media_type` | `REELS` or `STORIES` | `REELS` |
| `share_to_feed` | Also show the Reel in the feed | `true` |
| `share_mode` | Trial Reels: `CUSTOM`, `TRIAL_REELS_SHARE_TO_FOLLOWERS_IF_LIKED`, `TRIAL_REELS_DONT_SHARE_TO_FOLLOWERS` | `CUSTOM` |
| `cover_url` / `cover_image` | Custom Reel cover (URL or binary JPEG ≤ 8 MB) | none |
| `user_tags` | Users to tag, e.g. `"@user1, user2"` (video posts) | none |
| `collaborators` | Comma-separated collaborator usernames | none |
| `location_id` | Instagram location ID | none |
| `first_comment` | Auto-post a first comment after publishing | none |
| `scheduled_date` | ISO-8601 date to [schedule the post](../api/schedule-posts.md) | none |

Stories: send `media_type="STORIES"` on either endpoint (video or photo).

## Limits and gotchas

- **Account type:** personal Instagram accounts aren't supported by the Instagram API. Switch to Business/Creator and link a Facebook Page. Error 400 on connect usually means a missing Page or unverified account ([FAQ](../resources/faq.md)).
- **Daily cap:** 50 Instagram posts per connected account per rolling 24 h ([upload limits](./limit-of-uploads.md)).
- **Video specs:** max 300 MB. See [Video Requirements](../api/video-requirements.md) and [Photo Requirements](../api/photo-requirements.md).
- **Photo tagging** uses JSON `user_tags` with x/y coordinates; see [Upload Photo](../api/upload-photo.md).


---
# How to Post to LinkedIn with an API
URL: https://docs.upload-post.com/guides/post-to-linkedin-api

# How do I post to LinkedIn with an API?

Send a request to Upload-Post with `platform[]=linkedin`: `POST https://api.upload-post.com/api/upload` for video, `POST /api/upload_photos` for images, `POST /api/upload_text` for text-only posts and `POST /api/upload_document` for native PDF/PPT/DOC document posts. Upload-Post uses its own approved LinkedIn app, so you skip the LinkedIn developer-program application and the Marketing API access request entirely. Connect the LinkedIn account once via OAuth, then post to the member profile or to any company page they administer.

## Steps

1. Create an account at [upload-post.com](https://www.upload-post.com) and generate an API key under **API Keys** ([Authentication](./authentication.md)).
2. Connect LinkedIn at [Manage Users](https://app.upload-post.com/manage-users) (or via a [white-label JWT link](./user-profile-integration.md) for your users).
3. Post a video:

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="Your Video Title"' \
  -F 'description="Post commentary shown above the video"' \
  -F 'user="test"' \
  -F 'platform[]=linkedin' \
  -X POST https://api.upload-post.com/api/upload
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload",
    headers={"Authorization": "Apikey your-api-key-here"},
    files={"video": open("/path/to/your/video.mp4", "rb")},
    data={
        "title": "Your Video Title",
        "description": "Post commentary shown above the video",
        "user": "test",
        "platform[]": "linkedin",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript

const form = new FormData();
form.append("video", new Blob([fs.readFileSync("/path/to/your/video.mp4")]), "video.mp4");
form.append("title", "Your Video Title");
form.append("description", "Post commentary shown above the video");
form.append("user", "test");
form.append("platform[]", "linkedin");

const response = await fetch("https://api.upload-post.com/api/upload", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

## Post as a company page

By default posts go to the member's personal profile. To post as an organization, pass `target_linkedin_page_id` with the page ID from [Get LinkedIn Pages](../api/get-linkedin-pages.md):

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=linkedin' \
  -F 'title="Our company is launching a new product!"' \
  -F 'target_linkedin_page_id="your_linkedin_page_id_here"' \
  -X POST https://api.upload-post.com/api/upload_text
```

## Text posts and documents

- **Text-only** posts (optionally with a link preview via `linkedin_link_url`): [Upload Text](../api/upload-text.md).
- **Documents** (PDF, PPT/PPTX, DOC/DOCX rendered as a native LinkedIn carousel viewer): [Upload Document](../api/upload-document.md), aka `POST /api/upload_document`.

## Useful LinkedIn parameters

Full list in the [Upload Video reference](../api/upload-video.md#linkedin):

| Parameter | What it does | Default |
|-----------|--------------|---------|
| `linkedin_title` | LinkedIn-specific title (falls back to `title`) | `title` |
| `linkedin_description` / `description` | Sent as the LinkedIn commentary | `title` |
| `visibility` | `PUBLIC`, `CONNECTIONS`, `LOGGED_IN`, `CONTAINER` | `PUBLIC` |
| `target_linkedin_page_id` | Post as an organization page | none |
| `first_comment` / `linkedin_first_comment` | Auto-post a first comment | none |
| `scheduled_date` | ISO-8601 date to [schedule the post](../api/schedule-posts.md) | none |

## Limits and gotchas

- **Daily cap:** 150 LinkedIn posts per connected account per rolling 24 h ([upload limits](./limit-of-uploads.md)).
- **Video specs:** up to 5 GB / 10 min. See [Video Requirements](../api/video-requirements.md).
- **Token expiry:** LinkedIn tokens last ~60 days and auto-refresh with regular use ([FAQ](../resources/faq.md)).


---
# How to Post to TikTok with an API
URL: https://docs.upload-post.com/guides/post-to-tiktok-api

# How do I post to TikTok with an API?

Send a `multipart/form-data` request to Upload-Post's `POST https://api.upload-post.com/api/upload` endpoint with your video file (or a public video URL), a `user` profile and `platform[]=tiktok`. Upload-Post runs its own approved TikTok app, so there's no developer application to register, no TikTok audit to pass, and no OAuth tokens to manage. You connect the TikTok account once in the dashboard and that's it. One caveat: TikTok posting needs a paid plan, it's not available on Free.

## Steps

1. Create an account at [upload-post.com](https://www.upload-post.com) and generate an API key in the [dashboard](https://app.upload-post.com/) under **API Keys** (see [Authentication](./authentication.md)).
2. Connect a TikTok account to a profile at [Manage Users](https://app.upload-post.com/manage-users). It's a standard TikTok login, no developer setup. For posting on behalf of *your* users, use [white-label JWT connect links](./user-profile-integration.md).
3. Call the [upload endpoint](../api/upload-video.md):

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="Your Video Title"' \
  -F 'user="test"' \
  -F 'platform[]=tiktok' \
  -X POST https://api.upload-post.com/api/upload
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload",
    headers={"Authorization": "Apikey your-api-key-here"},
    files={"video": open("/path/to/your/video.mp4", "rb")},
    data={
        "title": "Your Video Title",
        "user": "test",
        "platform[]": "tiktok",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript

const form = new FormData();
form.append("video", new Blob([fs.readFileSync("/path/to/your/video.mp4")]), "video.mp4");
form.append("title", "Your Video Title");
form.append("user", "test");
form.append("platform[]", "tiktok");

const response = await fetch("https://api.upload-post.com/api/upload", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

`video` also accepts a public URL instead of a file: `-F 'video="https://example.com/videos/myvideo.mp4"'`.

## Useful TikTok parameters

All parameters are documented in the [Upload Video reference](../api/upload-video.md#tiktok). The most used ones:

| Parameter | What it does | Default |
|-----------|--------------|---------|
| `tiktok_title` | TikTok-specific caption (falls back to `title`). Max 2,200 chars for video. | `title` |
| `post_mode` | `DIRECT_POST` publishes immediately; `MEDIA_UPLOAD` sends the video to the user's TikTok inbox/drafts. | `DIRECT_POST` |
| `privacy_level` | `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY` | `PUBLIC_TO_EVERYONE` |
| `disable_comment` / `disable_duet` / `disable_stitch` | Turn off comments, duets or stitches | `false` |
| `cover_timestamp` | Video frame (ms) to use as cover | `1000` |
| `is_aigc` | Declare AI-generated content | `false` |
| `scheduled_date` | ISO-8601 date to [schedule the post](../api/schedule-posts.md) | none |
| `async_upload` | Return immediately with a `request_id` and process in background ([recommended](./async-uploads.md)) | `false` |

:::tip Draft mode gets better organic reach
We recommend `post_mode=MEDIA_UPLOAD` (Draft): the video lands in the TikTok inbox and the user publishes it from the app, which typically performs better in TikTok's distribution. In Draft mode TikTok ignores title/privacy metadata sent via API.
:::

## TikTok photo slideshows

Post images (with optional automatic music) through the [Upload Photos endpoint](../api/upload-photo.md):

```bash
curl -X POST https://api.upload-post.com/api/upload_photos \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'photos[]=@photo1.jpg' \
  -F 'photos[]=@photo2.jpg' \
  -F 'title="Photo slideshow with music"' \
  -F 'user="test"' \
  -F 'platform[]=tiktok' \
  -F 'auto_add_music=true'
```

## Limits and gotchas

- **Daily cap:** 15 TikTok posts per connected account per rolling 24 h ([upload limits](./limit-of-uploads.md)).
- **Free plan:** TikTok uploads return `403`; you need a paid plan ([pricing & limits](../resources/pricing-and-limits.md)).
- **`reached_active_user_cap` error:** a temporary TikTok platform limit. See the [workaround guide](./reached-active-user-cap-error.md).
- **Formats:** see [Video Requirements](../api/video-requirements.md) for TikTok's accepted formats and sizes.


---
# How to Post to X (Twitter) with an API
URL: https://docs.upload-post.com/guides/post-to-x-twitter-api

# How do I post to X (Twitter) with an API?

Send a request to Upload-Post with `platform[]=x`: `POST https://api.upload-post.com/api/upload_text` for tweets and threads, `POST /api/upload` for video and `POST /api/upload_photos` for images. Upload-Post posts through its own X API access, so you don't need to buy an X API tier or manage X OAuth yourself. Just connect the X account once in the dashboard. Text longer than 280 characters is automatically split into a well-formatted thread (override with `x_long_text_as_post=true`).

## Steps

1. Create an account at [upload-post.com](https://www.upload-post.com) and generate an API key under **API Keys** ([Authentication](./authentication.md)).
2. Connect the X account at [Manage Users](https://app.upload-post.com/manage-users) (or via a [white-label JWT link](./user-profile-integration.md) for your users).
3. Post a tweet:

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'user="test"' \
  -F 'platform[]=x' \
  -F 'title="This is my tweet content!"' \
  -X POST https://api.upload-post.com/api/upload_text
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload_text",
    headers={"Authorization": "Apikey your-api-key-here"},
    data={
        "user": "test",
        "platform[]": "x",
        "title": "This is my tweet content!",
    },
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript
const form = new FormData();
form.append("user", "test");
form.append("platform[]", "x");
form.append("title", "This is my tweet content!");

const response = await fetch("https://api.upload-post.com/api/upload_text", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

## Threads, video and images

- **Threads:** just send long text. Paragraphs are grouped up to 280 chars per tweet and posted as a thread, with media attached to the first tweet. Details in [Upload Text](../api/upload-text.md).
- **Video:** `POST /api/upload` with `platform[]=x` ([Upload Video](../api/upload-video.md#x-twitter)).
- **Images:** `POST /api/upload_photos` takes up to 4 images per tweet; more images are split across a thread controlled by `x_thread_image_layout` ([Upload Photo](../api/upload-photo.md)).

## Useful X parameters

Full list in the [Upload Video](../api/upload-video.md#x-twitter) and [Upload Text](../api/upload-text.md) references:

| Parameter | What it does | Default |
|-----------|--------------|---------|
| `x_title` | X-specific text (falls back to `title`) | `title` |
| `x_long_text_as_post` | Publish long text as a single post instead of a thread | `false` |
| `reply_to_id` | Reply to an existing tweet | none |
| `community_id` | Post into an X community | none |
| `reply_settings` | Who can reply: `following`, `mentionedUsers`, `subscribers`, `verified` | none |
| `tagged_user_ids` | Tag up to 10 users in media | `[]` |
| `first_comment` / `x_first_comment` | Auto-reply under the post | none |
| `scheduled_date` | ISO-8601 date to [schedule the post](../api/schedule-posts.md) | none |

:::warning URLs are stripped from X posts by default
X bills ~$0.200 per post containing a URL vs $0.015 without (13×), so Upload-Post removes clickable URLs from captions, titles and first comments on every X post. To publish posts **with** links, enable the [X Links add-on](./x-links-addon.md). Details: [Character Limits](../resources/character-limits.md#x-twitter-character-limits).
:::

## Limits and gotchas

- **Daily cap (per connected X profile, rolling 24 h) is per plan:** Free/Basic 10, Professional 20, Advanced/Business 30 ([upload limits](./limit-of-uploads.md)).
- **Quote tweets with media are rejected.** Post the quote as text-only via Upload Text ([details](../api/upload-video.md#x-twitter)).
- **Replies can 403** on X's Pay-Per-Use tier when the account never engaged with the author ([details](../api/upload-video.md#x-twitter)).


---
# How to Upload Videos to YouTube with an API
URL: https://docs.upload-post.com/guides/post-to-youtube-api

# How do I upload videos to YouTube with an API?

Send a `multipart/form-data` request to Upload-Post's `POST https://api.upload-post.com/api/upload` with the video file (or URL), a `title` (**required** for YouTube), `user` and `platform[]=youtube`. Upload-Post ships its own dedicated YouTube API quota, so you don't need a Google Cloud project, YouTube Data API keys or a quota-increase request. You just connect the YouTube channel once via Google OAuth in the dashboard.

## Steps

1. Create an account at [upload-post.com](https://www.upload-post.com) and generate an API key under **API Keys** ([Authentication](./authentication.md)).
2. Connect the YouTube channel at [Manage Users](https://app.upload-post.com/manage-users) via Google OAuth (or a [white-label JWT link](./user-profile-integration.md) for your users).
3. Upload:

<Tabs groupId="lang">
<TabItem value="curl" label="cURL">

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="Your Video Title"' \
  -F 'description="Your video description"' \
  -F 'user="test"' \
  -F 'platform[]=youtube' \
  -F 'tags[]=tutorial' \
  -F 'tags[]=howto' \
  -X POST https://api.upload-post.com/api/upload
```

</TabItem>
<TabItem value="python" label="Python">

```python
import requests

response = requests.post(
    "https://api.upload-post.com/api/upload",
    headers={"Authorization": "Apikey your-api-key-here"},
    files={"video": open("/path/to/your/video.mp4", "rb")},
    data=[
        ("title", "Your Video Title"),
        ("description", "Your video description"),
        ("user", "test"),
        ("platform[]", "youtube"),
        ("tags[]", "tutorial"),
        ("tags[]", "howto"),
    ],
)
print(response.json())
```

</TabItem>
<TabItem value="js" label="JavaScript">

```javascript

const form = new FormData();
form.append("video", new Blob([fs.readFileSync("/path/to/your/video.mp4")]), "video.mp4");
form.append("title", "Your Video Title");
form.append("description", "Your video description");
form.append("user", "test");
form.append("platform[]", "youtube");
form.append("tags[]", "tutorial");
form.append("tags[]", "howto");

const response = await fetch("https://api.upload-post.com/api/upload", {
  method: "POST",
  headers: { Authorization: "Apikey your-api-key-here" },
  body: form,
});
console.log(await response.json());
```

</TabItem>
</Tabs>

## YouTube Shorts

There is no separate Shorts endpoint: YouTube automatically classifies a video as a Short when it is **60 seconds or less** and **vertical (9:16) or square (1:1)**. Upload it exactly like any other video.

## Useful YouTube parameters

Full list in the [Upload Video reference](../api/upload-video.md#youtube):

| Parameter | What it does | Default |
|-----------|--------------|---------|
| `youtube_title` / `youtube_description` | YouTube-specific title/description (fall back to `title`) | `title` |
| `tags[]` | Video tags | `[]` |
| `categoryId` | YouTube category | `"22"` |
| `privacyStatus` | `public`, `unlisted` or `private` | `public` |
| `thumbnail` / `thumbnail_url` | Custom thumbnail (file or URL, ≤ 2 MB; **not supported for Shorts**) | none |
| `youtube_playlist_id` | Playlist ID(s) to add the video to after publishing (comma-separated) | none |
| `youtube_subtitle_file_{N}` + `youtube_subtitle_language_{N}` | Subtitle tracks (SRT/VTT/…) | none |
| `containsSyntheticMedia` | Declare AI-generated content | `false` |
| `selfDeclaredMadeForKids` | COPPA declaration | `false` |
| `scheduled_date` | ISO-8601 date to [schedule the post](../api/schedule-posts.md) | none |

## Limits and gotchas

- **`title` is required** for YouTube uploads.
- **Daily cap:** YouTube videos are capped per connected channel per rolling 24 h; see [upload limits](./limit-of-uploads.md).
- **Big files:** use [`async_upload=true`](./async-uploads.md) and poll [Upload Status](../api/upload-status.md); synchronous requests fall back to background after 59 s.
- **Quota:** you no longer need your own Google Cloud quota ([background here](./youtube-quota-explained.md)).
- **Formats:** see [Video Requirements](../api/video-requirements.md).


---
# Rate Limits & Polling Best Practices
URL: https://docs.upload-post.com/guides/rate-limits

# Rate Limits & Polling Best Practices

Upload-Post applies rate limits at multiple levels to protect the service and the social media platforms. This guide explains each limit and how to design your integration to work within them.

## API Rate Limits

Every authenticated API response includes rate limit headers:

| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Maximum requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |

When you exceed the limit, the API returns **HTTP 429 Too Many Requests**. Wait until the `X-RateLimit-Reset` timestamp before retrying.

## Upload Status Polling

When using `async_upload=true`, you need to poll the [Upload Status](../api/upload-status) endpoint to get the result. Here are the recommended intervals:

### Polling Strategy

```
1. Submit upload → get request_id
2. Wait 5 seconds (initial delay)
3. Poll GET /api/uploadposts/status?request_id=<ID>
4. If status is "pending", "queued", or "processing" → wait 10 seconds → repeat step 3
5. If status is "completed" or "failed" → done
```

### Recommended Polling Intervals

| Scenario | Poll Interval | Max Wait |
|----------|--------------|----------|
| Photo uploads | Every 5–10 seconds | 2 minutes |
| Video uploads (small, < 50 MB) | Every 10 seconds | 5 minutes |
| Video uploads (large, > 50 MB) | Every 15 seconds | 10 minutes |
| Scheduled posts (after scheduled time) | Every 30 seconds | 10 minutes |
| Queued posts | Every 60 seconds | Until next queue slot |

### Status Cache TTLs

The status endpoint uses internal caching. Here is how often the status value is refreshed:

| Status | Cache TTL | Meaning |
|--------|-----------|---------|
| `queued` | 2 seconds | The upload is waiting for a worker |
| `pending` | 2 seconds | Accepted but not yet started |
| `processing` | 3 seconds | At least one platform is actively uploading |
| `completed` | 5 minutes | All platforms finished successfully |
| `failed` | 5 minutes | All platforms failed |

**Key takeaway:** Polling faster than every 5 seconds for non-terminal states is unnecessary — the cache refreshes every 2–3 seconds. For terminal states (`completed`/`failed`), the result is cached for 5 minutes, so subsequent polls are fast.

### Alternative: Use Webhooks

Instead of polling, configure a [webhook](../api/webhooks) to receive a `POST` notification when the upload completes. This is more efficient for high-volume integrations:

```json
{
  "channels": { "webhook": true },
  "webhook_url": "https://your-server.com/webhook"
}
```

The webhook fires once per platform per upload, so you get immediate notification without polling.

## Upload Rate Limits

### Per-Request Duplicate Protection

The API prevents duplicate uploads using idempotency keys and rate limiting:

- **Same content to same platform**: If you submit an identical upload (same user, platform, and content hash) within a short window, the API returns the existing request instead of creating a duplicate.
- **Idempotency-Key header**: Send a unique `Idempotency-Key` or `X-Idempotency-Key` header to ensure exactly-once processing, even across retries.

### Daily Upload Caps (per platform per account)

Each social account has a daily hard cap based on platform limits. Exceeding these returns **HTTP 429**:

| Platform | Max posts / 24h |
|----------|----------------|
| Instagram | 50 |
| TikTok | 15 |
| LinkedIn | 150 |
| YouTube | 10 |
| Facebook | 25 |
| X (Twitter) | 50 |
| Threads | 50 |
| Pinterest | 20 |
| Reddit | 40 |
| Bluesky | 50 |

These are rolling 24-hour windows per social account, not per API key.

### Monthly API Usage

Your plan determines how many API upload calls you can make per month:

| Plan | Monthly uploads |
|------|----------------|
| Free | 10 |
| Paid plans | See [pricing](https://upload-post.com/pricing) |

Check your current usage with `GET /api/uploadposts/me` — the response includes `api_usage.count`.

## API Key Brute-Force Protection

Failed authentication attempts (invalid API keys) are rate-limited per IP:

- After **10 consecutive failed attempts** with the same invalid key, the IP is blocked for **5 minutes**.
- The API returns **HTTP 429** during the block period.

## Best Practices

1. **Use webhooks** instead of polling when possible — they're instant and don't consume rate limit budget.
2. **Respect `X-RateLimit-Remaining`** — stop sending requests when it reaches 0.
3. **Use exponential backoff** on 429 responses — don't hammer the API after hitting a limit.
4. **Set `async_upload=true`** for all uploads — synchronous uploads timeout after 59 seconds anyway.
5. **Send `Idempotency-Key`** for all upload requests — this protects you from duplicate posts if your HTTP client retries on timeout.
6. **Poll at the recommended intervals** — faster polling just returns cached results and wastes your rate limit budget.


---
# Error: Reached Active User Cap
URL: https://docs.upload-post.com/guides/reached-active-user-cap-error

# Error: `{"code":"reached_active_user_cap"}`

If you've encountered this error, don't worry. This is not an issue with your account, your content, or our platform's stability. It's a temporary limitation from the TikTok API.

This error means that the **daily limit of active users** allowed by TikTok for our application has been reached.

:::tip Workaround that works right now: post to your TikTok inbox

You don't have to wait. You can keep publishing today by switching the upload to **`post_mode=MEDIA_UPLOAD`**.

Instead of publishing directly, the content is sent to your **TikTok inbox**. You then open the TikTok app, where the upload is waiting, add the final touches and publish it yourself. This path is **not affected by the daily active-user cap**, and it works for both **videos and photos**.

**How to use it:** add the `post_mode` field set to `MEDIA_UPLOAD` to your upload request.

```bash
curl --location 'https://api.upload-post.com/api/upload' \
  --header 'Authorization: ApiKey YOUR_API_KEY' \
  --form 'user="your_profile"' \
  --form 'platform[]="tiktok"' \
  --form 'title="My caption"' \
  --form 'video=@"/path/to/video.mp4"' \
  --form 'post_mode="MEDIA_UPLOAD"'
```

For photos, send the same `post_mode="MEDIA_UPLOAD"` field to the [photo upload endpoint](/api/upload-photo).

**What to expect with `MEDIA_UPLOAD`:**
- The post lands in your **TikTok inbox / drafts** — you must open the **TikTok app** to confirm and publish it.
- In this mode TikTok does **not** allow setting the title, caption, privacy or other metadata via the API. You add all of that manually in the app before publishing.
- See the full reference in the [video](/api/upload-video) and [photo](/api/upload-photo) API docs.

:::

### What is a "Daily Active User"?

In this context, a "daily active user" is anyone who uses our application to interact with the TikTok API on a given day. TikTok sets a cap on how many unique users can do this through a single application (like ours) within a 24-hour period.

### What should you do?

*   **Your account and content are safe.** This is not a penalty or a block on your account.
*   **Keep posting today with `post_mode=MEDIA_UPLOAD`.** This sends your content to your TikTok inbox so you can publish it from the app, bypassing the cap (see the workaround above).
*   **Or wait and retry.** The user cap is reset by TikTok every 24 hours, so waiting a few hours before posting directly again also works.
*   If the error persists for more than 24 hours, please try again the next day.

### Why does this happen?

To manage their platform's resources, TikTok imposes a daily usage quota on every application that connects to its API. Due to the rapid growth of our user community, we are sometimes hitting this maximum allowed number of daily users.

### What are we doing about it?

We are actively working on a solution. We are in direct communication with TikTok's developer support team to **request an increase in our daily user quota**.

Unfortunately, the timeline for this increase is determined by TikTok, and we cannot expedite their internal review process. We appreciate your patience as we work to resolve this for good.

Thank you for your understanding. We are committed to providing a reliable service and are doing everything we can to support our growing community.


---
# White-label Integration
URL: https://docs.upload-post.com/guides/user-profile-integration

# White-label Integration Guide

![Profiles diagram](/img/profiles-diagram.png)

This guide explains how to integrate Upload-Post directly into your own platform. This allows your users to connect their social media accounts securely through Upload-Post, enabling your platform to manage their profiles and posts via the API on their behalf.

## Integration Flow Overview

The core idea is to create a unique profile within Upload-Post for each user on your platform who wants to connect their social accounts. You then let the user link their accounts in one of two ways:

*   **Hosted connect page** (fastest): generate a secure `access_url` and send the user there. You can brand it with your logo, title, texts and language — no frontend work needed. This is the flow described step by step below.
*   **Your own connect page** ([Connect API](../api/connect-api.md)): build the connection UI inside your product, on your own domain, with your own design. Your page requests each platform's authorize URL from the API and Upload-Post handles the OAuth exchange behind the scenes.

Once linked (either way), your platform interacts with the Upload-Post API using the user's unique identifier.

## Step-by-Step Integration

### Step 1: Create a User Profile

For each user on your platform, you need to create a corresponding profile in Upload-Post. This is done by making a `POST` request to the `/api/uploadposts/users` endpoint.

*   **Requirement:** You must provide a unique `username` in the request body. This `username` should be a stable identifier that links the Upload-Post profile back to the user on your platform (e.g., your internal user ID).
*   **Authentication:** Remember to include your `Authorization: Apikey YOUR_API_KEY` header.
*   **Result:** The API will respond with details of the created profile, confirming the `username`.

➡️ **See details:** [Create User Profile API Reference](../api/user-profiles.md#create-user-profile)

### Step 2: Generate the Secure JWT URL

Once the profile exists, you need to generate a secure URL that your user will use to connect their social media accounts. Make a `POST` request to the `/api/uploadposts/users/generate-jwt` endpoint.

*   **Requirement:** In the request body, provide the same unique `username` (from Step 1). You can also include the following optional fields:
    *   `redirect_url`: A URL to which the user will be redirected after linking their account.
    *   `logo_image`: A URL to a logo image for branding on the linking page.
    *   `redirect_button_text`: (Optional) The text to display on the redirect button after linking. Defaults to "Logout connection".
    *   `connect_title`: (Optional) Custom title text for the connection page.
    *   `connect_description`: (Optional) Custom description text for the connection page.
    *   `platforms`: (Optional) List of platforms to show for connection. Defaults to all supported platforms.
    *   `show_calendar`: (Optional) Whether to show the calendar view on the connection page. Defaults to `true`.
    *   `readonly_calendar`: (Optional) When `true`, shows only a read-only calendar view. Users cannot edit, delete, or create posts, and cannot connect or disconnect social accounts. Ideal for sharing a content calendar with end clients. Defaults to `false`.
    *   `language`: (Optional) Forces the connection page language for this profile. Supported values: `en`, `es`, `de`, `fr`, `pt`, `pl`, `tr`. When omitted, the page auto-detects the visitor's browser language and falls back to English.
    *   `ui_labels`: (Optional) A flat object of connect-page i18n keys → replacement strings, for white-label integrations that need to override UI text beyond `connect_title`/`connect_description`/`redirect_button_text`. Max 100 entries, values max 300 chars. See [Custom UI Labels](../api/user-profiles.md#custom-ui-labels).
*   **Authentication:** Include your `Authorization: Apikey YOUR_API_KEY` header.
*   **Result:** The API will return a JSON object containing an `access_url`. This URL contains a secure token (JWT) valid for 48 hours.

➡️ **See details:** [Generate JWT URL API Reference](../api/user-profiles.md#endpoint-generate-jwt-url)

**Quick troubleshooting (common integration mistakes):**
- If `generate-jwt` returns `404`, call `GET /api/uploadposts/users` and verify the profile `username` exists.
- If profile creation returns `403`, you hit your plan profile limit. Resolve limits before retrying JWT generation.
- Always treat profile creation and JWT generation as two explicit checked steps (do not ignore non-2xx responses).

### Step 3: User Connects Accounts

Redirect your user to the `access_url` obtained in Step 2. This URL will open the Upload-Post connection interface, guiding the user through the process of securely connecting their desired social media accounts (like Instagram, TikTok, Facebook, etc.) to their profile.

**Enhanced Connect Experience:**
- **Professional Navigation**: Tab-based interface for easy switching between account connection and calendar view
- **Calendar View** (if enabled): Users can view their scheduled posts and upload history directly from the connect page
- **Customizable Interface**: You can control the branding, title, and available features through the JWT parameters
- **Secure OAuth Flows**: Upload-Post handles all authentication and token storage securely

The connection URL is valid for **48 hours**, giving users ample time to complete the linking process.

**Alternative — build your own connect page:** if you want full control over the connection experience (your domain, your design, your copy), skip the hosted page entirely and use the [Connect API](../api/connect-api.md). Your page calls `POST /api/uploadposts/oauth/{platform}/start` with the profile JWT from Step 2, redirects the user to the returned `authorize_url`, and Upload-Post sends them back to your `redirect_url` when the account is connected. The hosted page and the Connect API can be mixed freely — both link accounts to the same profile.

### Step 4: Manage User Content via API

After the user successfully connects their accounts in Step 3, your platform can now use other Upload-Post API endpoints to manage content on their behalf.

*   When making calls to endpoints like [Upload Photo](../api/upload-photo.md) or [Upload Video](../api/upload-video.md), you will typically include the user's unique `username` (the one you used in Step 1 and 2) in the request parameters to specify which profile's connected accounts should be used.
*   You can also retrieve the list of profiles and their connected accounts using the `GET /api/uploadposts/users` endpoint.

➡️ **See details:** [Get User Profiles API Reference](../api/user-profiles.md#get-user-profiles)

## Connecting Accounts (manual credentials)

Some platforms (Discord, Telegram, Slack, Mastodon, Nostr, Lemmy, Dev.to, Hashnode, WordPress, Whop, Listmonk) connect with a **manual credential** — an incoming webhook, API token, or key — instead of OAuth, with no browser redirect. The full step-by-step for each lives in its own guide:

➡️ **[Connecting Social Accounts](./connecting-accounts.md)**

## Read-Only Calendar for Clients

If you're an agency managing content for clients, you can generate a read-only calendar link that lets your clients view their scheduled posts without being able to edit anything.

Use the `readonly_calendar` parameter when generating the JWT:

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "client_profile_123",
    "readonly_calendar": true,
    "logo_image": "https://youragency.com/logo.png",
    "connect_title": "Your Content Calendar"
  }'
```

The returned `access_url` will show:
- Social media channel for each post
- Scheduled date and time
- Visual (photo/video preview)
- Post text/caption

The client **cannot**:
- Edit, delete, or reschedule posts
- Connect or disconnect social accounts
- Access any other section of Upload-Post

This is ideal for agencies that need to share content calendars with end clients for approval or visibility.

## Authentication

All API requests related to user profile management (`/api/uploadposts/users` and `/api/uploadposts/users/generate-jwt`) require authentication using your API Key. Include it in the `Authorization` header for every request:

```
Authorization: Apikey YOUR_API_KEY
```

Replace `YOUR_API_KEY` with the actual API key provided to you.

*(Note: The `/api/uploadposts/users/validate-jwt` endpoint uses Bearer token authentication, as detailed in its specific documentation).*

## Next Steps

With the user profiles created and accounts linked, explore the other API references to start managing content:

*   [Upload Photo API Reference](../api/upload-photo.md)
*   [Upload Video API Reference](../api/upload-video.md)


---
# X Links Add-on
URL: https://docs.upload-post.com/guides/x-links-addon

# X Links Add-on

By default, Upload-Post **strips every URL from posts published to X (Twitter)** before
they are published. This keeps your X usage on X's cheaper pricing tier — X bills roughly
`$0.200` per "Content: Create (with URL)" request versus `$0.015` for posts without a URL.
You can read the full stripping policy on the
[Character Limits page](../resources/character-limits.md#x-twitter-character-limits) and in the
[Upload Text reference](../api/upload-text.md).

The **X Links add-on** lets you keep URLs in your X posts instead of having them removed.

## What it does

When the add-on is active, parseable URLs are **no longer stripped** from your X posts —
captions, titles, and `first_comment` keep their links. This applies across every X path
(video, photo, text, scheduled, and retried posts).

Only posts that actually contain a link consume the add-on's quota. Normal link-free X posts
are unaffected and never count against it.

## Price

| Plan | Price | Billing |
| --- | --- | --- |
| X Links add-on | **$19/month (USD)** or **€18/month (EUR)** | Monthly only |

There is no annual option for this add-on yet.

## Quota: 50 link-posts per month

The add-on includes **50 link-posts per month**, counted **globally per account**. The 50/month
pool is **shared across all of your X profiles** — it is **not** a per-profile allowance.

Only posts that include a link consume the quota. Link-free posts do not.

### What happens when the cap is reached

When you reach your 50 link-posts for the billing cycle, the behaviour **reverts to the
default**: URLs are stripped from any further X posts until the quota resets at the start of
your next billing cycle.

## How to enable it

The X Links add-on requires an **active paid subscription** and is added on top of your
existing subscription:

1. Open your **subscription / billing settings** in the Upload-Post app.
2. Find the **X Links add-on** and choose **"Add to plan"**.

Once added, the add-on takes effect on your existing subscription and your monthly quota
begins.

## Coexists with other add-ons

The X Links add-on works alongside other add-ons (for example, the extra-profiles add-on).
You can have several add-ons active on the same subscription at once.


---
# Understanding YouTube API Quota Limits
URL: https://docs.upload-post.com/guides/youtube-quota-explained

# Understanding YouTube API Quota Limits

> **⚠️ DEPRECATED:**  
> We have now received our own dedicated YouTube API quota. You no longer need to configure your own Google Cloud project. All YouTube features are available directly from our platform without any extra setup.

We believe in being transparent with our community about the challenges we face and the solutions we implement. This document explains the current situation regarding YouTube's API quota and introduces a new feature that gives you more control.

### The Challenge: YouTube's API Quota

Every application that interacts with YouTube, including ours, is subject to a daily API quota. This quota determines how many actions (like uploads, comments, or data requests) can be performed through our platform each day.

Due to the incredible growth of our user base, we are frequently reaching the limit of our current quota. This can sometimes result in temporary service disruptions for YouTube-related features.

### What We Are Doing About It

For the past six months, we have been in ongoing discussions with YouTube's leadership team to request a significant increase in our daily quota. We believe a higher quota is essential to reliably serve our growing community.

Unfortunately, this process has been slower than anticipated, and we are still awaiting a final decision. We are persistently following up and providing all necessary information to make our case.

### Our Solution: Use Your Own Google Cloud Project

To provide a stable and reliable solution while we wait for the quota increase, we have implemented a new feature: **you can now connect your own Google Cloud project to our application.**

By doing this, you will use your own personal YouTube API quota instead of our shared, limited quota.

#### Benefits of This Approach:

*   **Reliability:** You are no longer affected by our shared quota reaching its limit. As long as your personal quota has not been exceeded, your YouTube actions will succeed.
*   **Control:** You have full visibility and control over your own API usage through your Google Cloud Console.
*   **No More Waiting:** This immediately solves the issue for you, without having to wait for our negotiations with YouTube to conclude.

### How to Connect Your Own Google Cloud Project

Here is a step-by-step guide to connect your own project and use your personal API quota:

1.  **Go to the [Google Cloud Console](https://console.cloud.google.com/).**
2.  **Create a new project** or select an existing one.
3.  **Enable the YouTube Data API v3** for your project. You can find this in the "APIs & Services" > "Library" section.
4.  Navigate to **"APIs & Services" > "Credentials"**.
5.  Click **"Create Credentials"** and select **"OAuth client ID"**.
6.  If prompted, configure the **"OAuth consent screen"**:
    *   Select **"External"** for the user type.
    *   Provide an app name (e.g., "My Upload-Post Connection"), your user support email, and developer contact information. You can use your own email address for all fields.
7.  For the **"Application type"**, choose **"Web application"**.
8.  Under **"Authorized redirect URIs"**, click **"ADD URI"** and paste the following URL:
    ```
    https://app.upload-post.com/youtube-callback
    ```
9.  Click **"Create"**. You will now see your **Client ID** and **Client Secret**.
10. **Copy these credentials.** You will need to enter them into our application to complete the connection.

After obtaining your Client ID and Client Secret, you can securely enter them in your account settings within our application to finalize the connection.

Thank you for your patience and understanding. We remain committed to resolving the quota issue at the platform level and will keep you updated on our progress.


---
# Upload-Post Social Media API
URL: https://docs.upload-post.com/introduction

# Upload-Post Social Media API

Welcome to Upload-Post

Upload-Post is your go-to API solution for seamless content management across multiple social media platforms. Our API simplifies the process of uploading and managing your social media content, making it easy for developers and creators to automate their social media presence.

## What is Upload-Post?

Upload-Post provides a streamlined API for uploading and managing content across popular social media platforms. Through our simple REST API, you can upload videos and photos to multiple platforms with minimal effort. Our platform handles all the complexities of social media APIs, allowing you to focus on creating great content.

The API follows REST principles, with endpoints representing different types of content uploads accessible via standard HTTP methods. All data is exchanged in JSON format, making integration straightforward and efficient.

## Supported Social Networks

Upload-Post currently supports 22 social media platforms and business tools:

### TikTok
- Upload videos
- Upload photo posts (multi-image carousels)
- Set the post text: videos take a `title`; photo posts take both a `title` and a `description`
- Set privacy level and commercial content disclosure
- Control comments, duets and stitches

### Instagram
- Upload photos and carousels
- Upload Reels and Stories
- Mixed photo + video carousels
- Tag collaborators, users and locations
- Add a first comment

### LinkedIn
- Share articles
- Post updates
- Upload images

### YouTube
- Upload videos
- Set video metadata
- Manage playlists

### Facebook
- Post updates
- Share links
- Upload photos and videos

### X (Twitter)
- Post tweets
- Upload media
- Thread support

### Threads
- Post text updates
- Upload photos (up to 10 per post, auto-threaded beyond that)
- Upload video
- Mixed photo + video carousels
- Add a topic tag and a first comment

### Pinterest
- Create image pins (single or carousel)
- Create video pins with a custom cover image
- Target a specific board
- Add a destination link and alt text

### Reddit
- Text posts, link posts, image posts and galleries
- Upload video
- Target a specific subreddit
- Set post flair
- Add a first comment with inline images

### Bluesky
- Post text updates
- Upload images
- Upload video
- Thread support
- Decentralized social networking

### Discord
- Post text messages to a channel
- Upload images (up to 10 per message)
- Upload video
- Connects via a channel incoming webhook (no OAuth)

### Telegram
- Post text messages to a chat or channel
- Upload images (single or album)
- Upload video
- Connects with your own bot (bot token + chat id, no OAuth)

### Google Business Profile
- Publish standard updates
- Create event posts
- Share offer posts with coupon codes
- Add call-to-action buttons (Book, Order, Shop, Learn More, Sign Up, Call)
- Attach photos to posts

### Slack
- Post text messages to any channel
- Connects via a channel Incoming Webhook URL (no OAuth)

### Mastodon
- Post text updates
- Upload images and video
- Works with any Mastodon instance (instance URL + access token)

### Nostr
- Publish NIP-01 kind:1 notes
- Signed with your nsec key and broadcast to your relays

### Lemmy
- Publish text posts
- Upload photos
- Works with any Lemmy community (instance, username and password)

### Dev.to
- Publish markdown articles to your DEV Community account
- Connects with a DEV API key

### Hashnode
- Publish markdown blog posts to your Hashnode publication
- Connects with a Personal Access Token and publication ID

### WordPress
- Create posts with text, photos and video on self-hosted WordPress
- Connects via an application password (WordPress REST API)

### Whop
- Send text posts to a Whop forum experience
- Connects with a Company API key

### Listmonk
- Create and dispatch email newsletter campaigns to a Listmonk list
- Connects with your instance URL, API user and list id

We integrate directly with each platform's official APIs to ensure reliable, secure, and compliant content management.

## Beyond Publishing

Upload-Post is not write-only. Alongside the upload endpoints you get:

- **Analytics**: per-account and per-post metrics. See [Get Analytics](./api/get-analytics.md).
- **Comments**: read the comments on a post, reply publicly, or reply privately by DM. See [Instagram Comments](./api/instagram-comments.md).
- **Direct messages**: send DMs and list conversations. See [Instagram DMs](./api/instagram-dms.md).
- **AutoDMs**: monitor a post for keywords in the comments and answer automatically. See [AutoDMs](./api/autodms.md).
- **Scheduling**: hand us a publish time instead of running your own cron queue.
- **Video processing**: FFmpeg jobs for adapting media per platform.

Comment and DM features are currently available for Instagram. Analytics cover Instagram, TikTok, YouTube, Threads, Pinterest, Reddit, Facebook, X (Twitter) and LinkedIn; Discord, Telegram, and the credential-based channels (Slack, Mastodon, Nostr, Lemmy, Dev.to, Hashnode, WordPress, Whop, Listmonk) do not expose analytics.

## MCP Server for AI Agents

Upload-Post ships an official, open-source **Model Context Protocol (MCP)** server, hosted at `https://mcp.upload-post.com/mcp`. Connect it to ChatGPT, claude.ai, Claude Desktop, Claude Code, Cursor, or any MCP-compatible agent, and your assistant can publish, schedule and analyze social media without you writing a REST client.

It supports both API key auth and OAuth 2.1 with PKCE, so agents can authorize on their own. See the [MCP Server guide](./guides/mcp-server-integration.md).

## API-First Approach

At Upload-Post, we believe in an API-first approach. This means:

- **Developer-Focused**: Everything is designed with developers in mind
- **Simple Integration**: Easy to integrate into any application
- **Clear Documentation**: Comprehensive guides and examples
- **Reliable Service**: Stable and secure API endpoints

## Getting Started with Upload-Post

1. **Register with Upload-Post**
   - Create your account at [upload-post.com](https://www.upload-post.com)
   - Get your API key from the dashboard

2. **Connect Social Accounts**
   - Link your social media accounts
   - Grant necessary permissions

3. **Explore the API**
   - Check out our [API Reference](./api/reference)
   - Try our [Quickstart Guide](./quickstart)

4. **Start Uploading**
   - Use our simple endpoints to upload content
   - Monitor your upload status
   - Manage your content across platforms

## Why Choose Upload-Post?

- **Simple Integration**: Get started in minutes with our straightforward API
- **Reliable Service**: Built on stable, production-ready infrastructure
- **Cost-Effective**: Start with 10 free uploads per month
- **Developer Support**: Comprehensive documentation and support
- **Secure**: Enterprise-grade security for your content and API keys

## Next Steps

- Check out our [Quickstart Guide](./quickstart) to make your first API call
- Explore our [API Reference](./api/reference) for detailed endpoint documentation

## Need Help?

- Contact our support team at info@upload-post.com
- Follow us on X (Twitter) [@vcaverog](https://x.com/vcaverog) for updates
- Check our [FAQ](./resources/faq) for common questions


---
# Quickstart Guide
URL: https://docs.upload-post.com/quickstart

# Quickstart Guide

This guide will help you get started with the Upload-Post API in minutes.

![Quick Start](/img/quick-start.png)

## Prerequisites

- An Upload-Post account
- Connected TikTok and/or Instagram accounts
- API key from your dashboard

## Step 1: Create Your Account

1. Visit [upload-post.com](https://www.upload-post.com)
2. Sign up for a new account

## Step 2: Connect Your Social Media Accounts

1. Navigate to [User Management](https://app.upload-post.com/manage-users)
2. Create a profile with a name of your choice (this name will be used in API calls)
3. Click on one of the social media networks
4. Follow the authentication flow for the selected platform
5. Grant necessary permissions for content upload

## Step 3: Generate Your API Key

1. Go to the API Keys section. [Api Keys](https://app.upload-post.com/api-keys)
2. Click "Generate New API Key"
3. Copy and save your API key securely

## Step 4: Make Your First API Call

### Upload a Video to TikTok

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="My First TikTok Video"' \
  -F 'user="test"' \
  -F 'platform[]=tiktok' \
  -X POST https://api.upload-post.com/api/upload
```

### Upload a Photo to Instagram

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'photos[]=@/path/to/your/image1.jpg' \
  -F 'user="test"' \
  -F 'platform[]=instagram' \
  -F 'title="My First Instagram Post"' \
  -F 'description="Hello Instagram!"' \
  -X POST https://api.upload-post.com/api/upload_photos
```

## Next Steps

- Check out our [API Reference](../api/reference) for detailed endpoint documentation
- Explore our [SDK Examples](../sdk-examples) for code samples in your preferred programming language

## Need Help?

- Check our [FAQ](resources/faq) for common questions
- Contact our support team at info@upload-post.com


---
# Social Media Character Limits
URL: https://docs.upload-post.com/resources/character-limits

# Social Media Character Limits

This guide summarizes the most relevant text limits for each social network supported by Upload-Post. Keep these constraints in mind when building payloads so posts are accepted without truncation.

## Platform-Specific Character Limits

### Facebook Character Limits

| Property | Description |
| --- | --- |
| post | 63,206 characters maximum |
| title | Reels title – 255 characters maximum |

### Instagram Character Limits

| Property | Description |
| --- | --- |
| post | 2,200 characters maximum |
| altText | 1,000 characters maximum per image |
| comment | 2,196 characters maximum |

### LinkedIn Character Limits

| Property | Description |
| --- | --- |
| post | 3,000 characters maximum |
| title | 400 characters maximum |
| comment | 1,250 characters maximum |

### TikTok Character Limits

| Property | Description |
| --- | --- |
| post | 2,200 characters maximum |
| title (photo posts) | 90 characters maximum |
| description (photo posts) | 4,000 characters maximum |

### Pinterest Character Limits

| Property | Description |
| --- | --- |
| post | 500 characters maximum |
| title | 100 characters maximum |
| link | 2,048 characters maximum |
| altText | 500 characters maximum |

### Reddit Character Limits

| Property | Description |
| --- | --- |
| post | 5,000 characters maximum |
| title | 300 characters maximum |
| comment | 10,000 characters maximum |

### Threads Character Limits

| Property | Description |
| --- | --- |
| post | 500 characters maximum |

### X (Twitter) Character Limits

| Property | Description |
| --- | --- |
| post | 280 characters maximum |
| post (Premium) | 25,000 characters maximum for Premium and Premium Plus accounts |
| altText | 1,000 characters maximum per image |
| subTitleName | 150 characters maximum |

:::warning URLs are automatically removed from X posts
Upload-Post strips every URL that X would turn into a clickable link from any
caption, title, description, or `first_comment` sent to X (Twitter), before
the post is published. This applies to video, photo, and text uploads via
every endpoint and via the scheduler.

**Why:** X charges `$0.200` per "Content: Create (with URL)" request versus
`$0.015` for posts without a URL — a 13× surcharge. To keep pricing predictable,
every parseable link is removed before the tweet leaves Upload-Post.

**What gets stripped:** anything X's parser recognises as a URL:

- Schemed URLs: `https://…`, `http://…`, `ftp://…`, `ws://…`
- `www.host.tld[/path]`
- Common shorteners: `t.co/…`, `bit.ly/…`, `tinyurl.com/…`, `lnkd.in/…`,
  `youtu.be/…`, etc.
- Bare hostnames with a path: `example.com/posts/1`
- IPv4 with a path: `192.168.1.1:8080/admin`
- Markdown link syntax: `[text](https://…)` (the URL portion is removed)

**What is NOT stripped:** anything that X does not parse as a link does not
trigger the surcharge, so we leave it alone — e.g. `example[.]com`,
`example(dot)com`, `hxxp://…`, unicode-dot lookalikes (`example．com`), or
bare domains with no path. These display as plain text in the tweet and are
billed at the normal `$0.015` rate.

If you need to share a link, put it in your X profile bio or render it visibly
inside an image/video. To keep URLs in your X posts instead of having them stripped,
see the [X Links add-on](../guides/x-links-addon.md).
:::

### Bluesky Character Limits

| Property | Description |
| --- | --- |
| post | 300 characters maximum |
| images | Up to 4 images per post |
| altText | Supported |

### Discord Character Limits

| Property | Description |
| --- | --- |
| message | 2,000 characters maximum (longer text is truncated) |
| attachments | Up to 10 images per message |

### Telegram Character Limits

| Property | Description |
| --- | --- |
| message (text) | 4,096 characters maximum (longer text is truncated) |
| caption (photo/video) | 1,024 characters maximum |
| album | Up to 10 photos per `sendMediaGroup` |

### YouTube Character Limits

| Property | Description |
| --- | --- |
| post | 5,000 characters maximum |
| youTubeOptions > title | 100 characters maximum |
| youTubeOptions > tags | 500 characters total, 2+ characters each |
| youTubeOptions > subTitleName | 150 characters maximum |

### Snapchat Character Limits

| Property | Description |
| --- | --- |
| Spotlight description | 500 characters maximum |
| Saved Story title | 45 characters maximum |

**Notes:**
- Stories are ephemeral (24 hours) and don't support text captions
- Saved Stories are permanent on your public profile
- Spotlight posts are permanent and reach wider audiences
- Only one media item (video or image) allowed per post
- Hashtags are supported and clickable in Spotlight posts

## Content Restrictions

### Banned Hashtags

### Google Business Profile Character Limits

| Field | Limit |
|-------|-------|
| Post summary (title) | 1,500 characters |
| Event title | 58 characters |
| Offer coupon code | 58 characters |
| Offer terms | 1,000 characters |
| CTA URL | Standard URL length |

**Notes:**
- Google Business Profile supports one photo per post via the API.
- Video uploads are not supported via the API.
- Product posts cannot be created via the API.

Upload-Post validates content against a list of prohibited hashtags before posting to Instagram. Posts containing any of these hashtags will be rejected with a validation error. The complete list of banned hashtags includes:

**A:** anorexia, alone, a$$, antivax, abdl, addmysc, adulting, always, armparty, asiagirl

**B:** beautyblogger, bikinibody, boho, blogladrona, brain, besties, bikinibod

**C:** costumes, curvygirls, cancer

**D:** date, dating, desk, dm

**E:** elevator, edm, endme

**F:** followtrain, followtrains

**G:** graffitiigers, girlsonly, gloves

**H:** hardworkpaysoff, happythanksgiving, humpday, hustler, hotgirls

**I:** iphonegraphy, italiano, ifb

**K:** kansas, killingit, kissing, kill, killme, killyourself, kys

**M:** master, models, mustfollow, milf, midget

**N:** nasty, newyearsday

**P:** petite, petitegirls, pushups, payme

**S:** saltwater, shit, shower, single, singlelife, skype, snap, snapchat, snapchatme, snowstorm, sopretty, stranger, streetphoto, sunbathing, swole, suicide, suicideawareness

**T:** tag4like, tanlines, teens, teen, thought, todayimwearing

**U:** undies, unbalanced

**V:** valentinesday

**W:** workflow

**Y:** youngmodel, yolo

If your content includes any of these hashtags, remove them before submitting your request to avoid validation errors.

## API Considerations

- Upload-Post validates payload sizes before sending them to social networks whenever limits are known. Requests that exceed the documented limits return a validation error.
- Some platforms might truncate overlong text instead of rejecting it (Meta products and YouTube occasionally do this). Inspect the per-platform response inside `results` to confirm the final content.
- For channels with strict limits such as X, consider shortening URLs in your application prior to calling the Upload-Post API.

## Updates and Changes

Social networks regularly adjust their limits. We keep this page aligned with the latest behavior we observe in production, but you should also:

- Check Upload-Post API responses for detailed error messages about rejected posts.
- Subscribe to our release notes for platform updates.
- Revisit this reference periodically, especially before large content campaigns.


---
# Common Errors
URL: https://docs.upload-post.com/resources/common-errors

# Common Errors

This guide covers the most common errors you might encounter when using Upload-Post and how to resolve them. Errors are organized by category to help you quickly find solutions.

## Session Expired {#session-expired}

Session and authentication errors occur when the connection between Upload-Post and your social media account has been broken. This is usually easy to fix by reconnecting your account.

### Common Session Errors

| Error Message | Platform | Solution |
|---------------|----------|----------|
| "Your [Platform] session has expired" | All | [Reconnect your account](https://app.upload-post.com/manage-users) |
| "Token expired and refresh failed" | All | [Reconnect your account](https://app.upload-post.com/manage-users) |
| "The session has been invalidated because the user changed their password" | Facebook/Instagram | Reconnect after password change |
| "Error validating access token" | Facebook/Instagram | Verify permissions and reconnect |
| "Your X session has expired and could not be refreshed" | X (Twitter) | Reconnect your X account |
| "User has not authorized application" | Various | Grant permissions and reconnect |
| "Unauthorized" | Various | Reconnect your account |

### How to Fix Session Errors

1. **Go to [Manage Users](https://app.upload-post.com/manage-users)**

2. **Find the affected account** - Look for accounts with warning indicators

3. **Disconnect the account** - Click the disconnect/remove button next to the account

4. **Reconnect the account** - Click "Connect" and complete the authorization flow

5. **Grant all permissions** - Make sure to approve all requested permissions during reconnection

:::tip
If you recently changed your password on a social media platform, you'll need to reconnect that account in Upload-Post.
:::

---

## Account Blocked {#account-blocked}

These errors occur when there's an issue with your account on the social media platform itself. Upload-Post cannot fix these - you need to resolve them directly on the platform.

### Common Account Status Errors

| Error Message | Platform | What It Means |
|---------------|----------|---------------|
| "Sessions for the user are not allowed because the user is not a confirmed user" | Facebook | Account needs verification |
| "The YouTube account of the authenticated user is suspended" | YouTube | Account suspended by YouTube |
| "Your account is temporarily locked" | X (Twitter) | X has locked your account |
| "The Instagram account is restricted or inactive" | Instagram | Instagram has restricted your account |
| "Action suspected as spam. Activity is restricted" | Instagram | Spam detection triggered |
| "The user used for authentication is suspended" | Various | Account suspended on platform |

### How to Fix Account Blocked Errors

These issues must be resolved directly on the social media platform:

#### Facebook/Instagram Account Not Confirmed

1. Go to [facebook.com](https://www.facebook.com) and log in
2. Check for any verification prompts or security notices
3. Go to **Settings & Privacy** > **Settings** > **Personal Details**
4. Verify your email and phone number are confirmed
5. Visit [Facebook Account Quality](https://www.facebook.com/accountquality/) to check for restrictions
6. After resolving issues, reconnect in Upload-Post

#### X (Twitter) Account Locked

1. Go to [twitter.com](https://twitter.com) or [x.com](https://x.com) and log in
2. Follow the prompts to unlock your account (may require phone verification)
3. Once unlocked, reconnect in Upload-Post

#### YouTube Account Suspended

1. Go to [YouTube](https://www.youtube.com) and sign in
2. Check the [YouTube Help Center](https://support.google.com/youtube/) for suspension appeals
3. Follow YouTube's process to restore your account

#### Instagram Restricted

1. Open the Instagram app and log in
2. Look for any notification banners or prompts
3. Follow Instagram's instructions to verify your identity
4. After restrictions are lifted, reconnect in Upload-Post

:::warning
Upload-Post cannot bypass platform restrictions. You must resolve these issues directly with the social media platform before reconnecting.
:::

---

## Configuration & Permissions {#configuration-permissions}

These errors occur when your account is connected but requires additional configuration or permissions.

### Common Configuration Errors

| Error Message | Platform | Solution |
|---------------|----------|----------|
| "No Facebook Pages found for your account" | Facebook | Connect a Facebook Page (personal profiles not supported) |
| "Multiple Facebook Pages found. Please select a Page" | Facebook | Select which Page to post to |
| "Facebook Page ID is required for text posts" | Facebook | Configure your profile to select a Page |
| "Couldn't get a Facebook Page access token for Page ID..." | Facebook | Reconnect with proper Page permissions |
| "Pinterest account not found or not configured" | Pinterest | Configure Pinterest in your profile |
| "Board not found" | Pinterest | Select a valid Pinterest board |
| "You are not permitted to access that resource" | Various | Check your role/permissions on the account |
| "This site doesn't allow you to save Pins" | Pinterest | The target site blocks Pinterest pins |
| "quote_tweet_id cannot be used when uploading media to X. Remove it and retry." | X (Twitter) | Quote tweets and media are mutually exclusive — drop `quote_tweet_id` or remove the media |
| "X rejected this reply (403)... not permitted to reply to this author on X's Pay-Per-Use tier" | X (Twitter) | Reply to an author the account has engaged with (this is a platform restriction, not a session error — reconnecting won't help) |

### How to Fix Configuration Errors

#### Facebook Page Issues

Facebook requires you to post to a **Facebook Page**, not a personal profile. The Facebook API does not support posting to personal profiles.

1. **Create a Facebook Page** (if you don't have one):
   - Go to [Create a Page](https://www.facebook.com/pages/create/)
   - Follow the setup wizard

2. **Connect your Page to Upload-Post**:
   - Go to [Manage Users](https://app.upload-post.com/manage-users)
   - Disconnect your Facebook account
   - Reconnect and select the Page you want to post to
   - Make sure you have **Admin** or **Editor** role on the Page

3. **Select the correct Page in your profile**:
   - Go to your Upload-Post profile settings
   - Select the Facebook Page from the dropdown

:::info Required Permissions
To post to a Facebook Page, you need:
- **Admin** or **Editor** role on the Page
- Grant "pages_manage_posts" permission during connection
- Grant "pages_read_engagement" permission
:::

#### Pinterest Board Issues

1. Make sure you have at least one board in your Pinterest account
2. Go to your Upload-Post profile and select the correct board
3. If the board was recently created, try disconnecting and reconnecting Pinterest

---

## Content Format {#content-format}

These errors occur when your media doesn't meet the platform's requirements for size, format, or aspect ratio.

### Common Content Errors

| Error Message | Platform | Solution |
|---------------|----------|----------|
| "Unsupported image size" | Various | Use supported dimensions |
| "Invalid image aspect ratio" | Instagram | Use ratio between 4:5 and 1.91:1 |
| "Video longer than 2 minutes" | X (Twitter) | Shorten video or upgrade X account |
| "TikTok rejected the media format" | TikTok | Use MP4/MOV format |
| "One or more tags are invalid" | Various | Remove special characters from tags |
| "Your post must contain post flair" | Reddit | Add required flair to your post |
| "Media could not be fetched from the provided URL" | Various | Use a publicly accessible URL |
| "Downloaded file is too small" | Various | Check URL points to valid media |
| "Collaborator usernames are invalid" | Instagram | Use valid public usernames without @ |

### Media Requirements by Platform

| Platform | Image Format | Max Image Size | Video Format | Max Video Duration |
|----------|--------------|----------------|--------------|-------------------|
| Instagram | JPG, PNG | 8 MB | MP4, MOV | 60 min (Feed), 90 sec (Reels) |
| Facebook | JPG, PNG, GIF | 10 MB | MP4, MOV | 240 min |
| TikTok | - | - | MP4, MOV | 10 min |
| X (Twitter) | JPG, PNG, GIF | 5 MB | MP4 | 2 min 20 sec* |
| LinkedIn | JPG, PNG | 8 MB | MP4 | 10 min |
| YouTube | - | - | MP4, MOV, AVI | 12 hours |
| Pinterest | JPG, PNG | 32 MB | MP4, MOV | 15 min |

*X Premium users may have longer video limits

### Instagram Aspect Ratio Guidelines

- **Square**: 1:1
- **Portrait**: 4:5 (recommended for Feed)
- **Landscape**: 1.91:1
- **Stories/Reels**: 9:16

Images outside the 4:5 to 1.91:1 range will be rejected.

:::tip
For best results, use **1080x1350 pixels (4:5 ratio)** for Instagram feed posts.
:::

### Fixing Media URL Issues

If you're getting "Media could not be fetched" errors:

1. **Check the URL is publicly accessible** - Open it in an incognito browser window
2. **Don't use private/restricted URLs** - Google Drive links must be set to "Anyone with the link"
3. **Use direct file URLs** - The URL should end with a file extension like `.jpg` or `.mp4`
4. **Check file size** - Very small files (< 1KB) often indicate a broken link

---

## Rate Limits {#rate-limits}

These errors occur when you've hit posting limits or the platform is experiencing temporary issues.

### Common Rate Limit Errors

| Error Message | Platform | Solution |
|---------------|----------|----------|
| "Daily upload limit exceeded for this channel" | YouTube | Wait until tomorrow (UTC midnight) |
| "Service Unavailable (503)" | Various | Retry in a few minutes |
| "Temporary issue. We retried 4 times but it still failed" | Instagram | Platform issue - retry later |
| "Fatal" / "Unexpected error" | Various | Platform issue - retry later |
| "Rate limit reached" | Various | Wait before making more requests |

### Platform Daily Limits

| Platform | Daily Posting Limit |
|----------|---------------------|
| Instagram | ~50 posts per day |
| TikTok | 15-20 videos per day |
| LinkedIn | ~150 posts per day |
| Pinterest | 25 pins per day |
| Reddit | Varies by subreddit |
| YouTube | 30 videos per day |

### TikTok Spam-Risk Errors {#tiktok-spam-risk}

TikTok enforces per-account anti-spam caps on API posting. These are **rolling 24-hour limits on your TikTok account**, not an Upload-Post restriction — no retry or reconnect can lift them early.

| Error | What it means | How to fix |
|-------|---------------|------------|
| `spam_risk_too_many_pending_share` | Your TikTok account has reached the cap of **5 unpublished (pending) inbox drafts in 24 hours**. This happens when videos are sent to the TikTok inbox but never published from the app. | Open the TikTok app and **publish or discard your pending drafts** (Inbox → System notifications), then wait for the 24h window to roll. |
| `spam_risk_too_many_posts` | Your TikTok account reached its daily API posting cap (typically **~15 posts per day per account**; the exact number varies per creator). | Wait for the rolling 24h window. Spread posts across the day or across accounts. |
| `spam_risk_user_banned_from_posting` | TikTok has banned this account from posting via API. | Contact TikTok support from the affected account. |

Official TikTok references: [Content Sharing Guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines) · [Content Posting API — Upload Video](https://developers.tiktok.com/doc/content-posting-api-reference-upload-video)

:::tip Inbox mode and pending drafts
If your posts are being delivered to the TikTok inbox (for example while the daily [active-user cap](/guides/reached-active-user-cap-error) fallback is active), remember to publish them from the app — 5 unpublished drafts will block further uploads for 24h with `spam_risk_too_many_pending_share`.
:::

### Bluesky Requirements {#bluesky-requirements}

| Error | What it means | How to fix |
|-------|---------------|------------|
| "Bluesky requires a verified email before uploading videos" (`unconfirmed_email`) | Bluesky-hosted accounts must have a **verified email** before the video service accepts uploads. Only the account owner can verify it. | In the Bluesky app: **Settings → Account → Email → Verify**, then retry the upload. |
| "Bluesky supports a maximum of 4 images per post" | The AT Protocol schema hard-limits image posts to **4 images**. | Split the post or reduce the selection to 4 images. |

Official Bluesky references: [Video uploads](https://docs.bsky.app/docs/tutorials/video) · [Creating posts](https://docs.bsky.app/docs/tutorials/creating-a-post)

### Handling Rate Limits

1. **Wait and retry** - Most limits reset at midnight UTC
2. **Spread posts throughout the day** - Avoid posting many items at once
3. **Use scheduling** - Schedule posts to spread them out automatically
4. **Check your usage** - View your posting history to track activity

:::info Automatic Rescheduling
When you hit a daily limit, Upload-Post may automatically reschedule your post for the next day. Check your scheduled posts to confirm.
:::

---

## Getting Help

If your error isn't listed here or you need additional assistance:

1. **Check our [FAQ](/resources/faq)** for general questions
2. **Review our [API Error Handling Guide](/guides/error-handling)** for technical details
3. **Contact support** at [info@upload-post.com](mailto:info@upload-post.com)

When contacting support, please include:
- The exact error message you received
- The platform you were posting to
- The type of content (text, photo, video)
- When the error occurred

---

## Platform Help Centers

If you need to resolve issues directly with a social media platform:

- [Facebook Help Center](https://www.facebook.com/help/)
- [Instagram Help Center](https://help.instagram.com/)
- [TikTok Support](https://support.tiktok.com/)
- [YouTube Help](https://support.google.com/youtube/)
- [X (Twitter) Help](https://help.x.com/)
- [LinkedIn Help](https://www.linkedin.com/help/linkedin)
- [Pinterest Help](https://help.pinterest.com/)
- [Reddit Help](https://support.reddithelp.com/)


---
# Frequently Asked Questions
URL: https://docs.upload-post.com/resources/faq

# Frequently Asked Questions

This FAQ covers the most common questions about Upload-Post. Questions are organized by category to help you find answers quickly.

---

## Connection & Authentication

### Where do I find my API Key?

1. Log in to your [Upload-Post Dashboard](https://app.upload-post.com/)
2. Navigate to the "API Keys" section
3. Click "Generate New API Key"
4. Copy and securely store your API key

Include your API key in the `Authorization` header of all API requests:

```bash
Authorization: Apikey your-api-key-here
```

### How do I connect my TikTok/Instagram/Facebook/YouTube/LinkedIn account?

1. Go to [Manage Users](https://app.upload-post.com/manage-users) in your dashboard
2. Click "Connect" next to the platform you want to add
3. Follow the OAuth authorization flow for that platform
4. Grant all requested permissions when prompted
5. Once connected, the account will appear in your profile

### How do I connect Discord?

Discord uses a **manual webhook** connection instead of OAuth:

1. In Discord, go to **Server Settings → Integrations → Webhooks → New Webhook**
2. Pick the channel to post to and click **Copy Webhook URL**
3. Send the webhook URL to Upload-Post:

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/discord/credentials \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"profile_username": "myprofile", "webhook_url": "https://discord.com/api/webhooks/123/abc..."}'
```

Once connected, post with `platform[]=discord`. Discord supports text (max 2000 chars), up to 10 images per message, and video. See [Connecting Discord](/guides/connecting-accounts#connecting-discord-manual-credentials).

### How do I connect Telegram?

Telegram uses your **own bot** instead of OAuth:

1. Message [@BotFather](https://t.me/BotFather) in Telegram, send `/newbot`, and copy the **bot token**
2. Add the bot to your target channel or group **as an administrator**
3. Send the bot token and chat id to Upload-Post:

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/telegram/credentials \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"profile_username": "myprofile", "bot_token": "123456:ABC...", "chat_id": "@my_channel"}'
```

Once connected, post with `platform[]=telegram`. Telegram supports text (max 4096 chars), single/multiple photos, and video (caption max 1024 chars). The bot **must be an admin** of the chat to post. See [Connecting Telegram](/guides/connecting-accounts#connecting-telegram-manual-credentials).

### Why do I get error 400 when connecting Instagram?

Error 400 when connecting Instagram usually means:

- **Account not confirmed**: Your Facebook/Instagram account needs email or phone verification. Go to [Facebook Account Quality](https://www.facebook.com/accountquality/) to check for issues.
- **Missing permissions**: During connection, make sure to approve ALL requested permissions.
- **Business account required**: Instagram API requires a Business or Creator account linked to a Facebook Page.
- **Page not connected**: Instagram must be linked to a Facebook Page. Create one at [facebook.com/pages/create](https://www.facebook.com/pages/create/) if needed.

### How do I get my Facebook Page ID?

Use our API endpoint to retrieve all Facebook Pages associated with your account:

```bash
curl 'https://api.upload-post.com/api/uploadposts/facebook/pages' \
  -H 'Authorization: Apikey YOUR_API_KEY'
```

The response includes `page_id`, `page_name`, and the associated profile for each page.

:::info
Facebook API does not support posting to personal profiles—only Pages can be posted to. If you don't have a Page, create one at [facebook.com/pages/create](https://www.facebook.com/pages/create/).
:::

### Why do I have to choose a Facebook Page every time I post?

Connecting Facebook only links your account—it does **not** pick a destination Page. You must **select the target Page on each upload** by passing `facebook_page_id`:

```bash
-F 'facebook_page_id="123456789"'
```

If exactly one Page is connected, it is auto-selected and you can omit the parameter. If multiple Pages are connected and you don't pass `facebook_page_id`, the API returns an `available_pages` list so you can pick one. Use [Get Facebook Pages](/api/get-facebook-pages) to look up your `page_id` values.

### How does JWT work for white-label integrations?

JWT (JSON Web Token) enables white-label integrations where your users connect their social accounts through your platform:

1. **Create a profile** for your user via `POST /api/uploadposts/users`
2. **Generate a secure URL** via `POST /api/uploadposts/users/generate-jwt`
3. **Redirect your user** to the returned `access_url`
4. The user connects their accounts through Upload-Post's interface
5. **Use the API** with the user's `username` to post on their behalf

The JWT URL is valid for **48 hours**. See the [White-label Integration Guide](/guides/user-profile-integration) for full details.

### How long do tokens last before I need to reconnect?

Token validity varies by platform:

| Platform | Typical Duration | Notes |
|----------|------------------|-------|
| TikTok | ~60 days | Auto-refreshes if used regularly |
| Instagram | ~60 days | May expire after password changes |
| Facebook | ~60 days | May expire after password changes |
| LinkedIn | ~60 days | Auto-refreshes if used regularly |
| YouTube | ~6 months | May require re-authorization |
| X (Twitter) | Long-lived | Usually stable unless revoked |

When a token expires, you'll receive a "session expired" error. Simply reconnect the account at [Manage Users](https://app.upload-post.com/manage-users).

---

## Limits & Quotas

### How many posts can I make per day on each platform?

Upload-Post enforces platform hard caps using a rolling 24-hour window to protect your accounts:

| Platform | Daily Limit (per account) |
|----------|---------------------------|
| Instagram | 50 posts |
| TikTok | 15 posts |
| LinkedIn | 150 posts |
| YouTube | 30 videos |
| Facebook | 25 posts |
| X (Twitter) | 50 posts |
| Threads | 50 posts |
| Pinterest | 20 pins |
| Reddit | 40 posts |
| Bluesky | 50 posts |

### What does error 429 (Too Many Requests) mean?

Error 429 indicates you've hit a rate limit. This can happen when:

1. **Monthly upload limit reached**: Your plan's monthly quota is exhausted
2. **Daily platform cap reached**: You've hit the 24-hour limit for a specific platform/account

Example response:
```json
{
  "success": false,
  "message": "Daily cap reached for instagram: 50/50 in last 24h",
  "violations": [{
    "platform": "instagram",
    "type": "hard_cap",
    "used_last_24h": 50,
    "cap": 50
  }]
}
```

**Solution**: Wait for the 24-hour window to roll over, or spread posts across multiple connected accounts.

### Are limits per account or per profile?

Limits are **per connected social account**, not per Upload-Post user or profile.

**Example**: If you manage 5 Profiles, and each has its own TikTok account connected, you get the full limit for each TikTok account (5 TikTok accounts × 15 posts = 75 TikTok posts per day total).

### What does "unlimited uploads" mean in the Basic plan?

"Unlimited uploads" refers to the number of API calls you can make per month through Upload-Post. However, you're still subject to:

- **Platform daily caps** (enforced per social account to protect against bans)
- **Profile limits** (based on your plan tier)
- **Platform-specific restrictions** (each social network's own rules)

### How many profiles can I connect on each plan?

Check [upload-post.com/pricing](https://www.upload-post.com/pricing) for the current limits per plan. You can see your current limit and usage via the `GET /api/uploadposts/users` endpoint, which returns a `limit` field showing your maximum allowed profiles.

---

## Video Uploads

### How do I upload YouTube Shorts vs regular videos?

YouTube automatically determines if a video is a Short based on:

- **Duration**: 60 seconds or less
- **Aspect ratio**: Vertical (9:16) or square (1:1)

Simply upload your video normally to the `/api/upload` endpoint. YouTube will classify it as a Short if it meets these criteria.

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -F 'video=@short_video.mp4' \
  -F 'title="My YouTube Short"' \
  -F 'user="myprofile"' \
  -F 'platform[]=youtube'
```

:::warning
Custom thumbnails are **not supported for YouTube Shorts**—they only apply to standard YouTube videos.
:::

### What is the maximum video file size?

Maximum file sizes vary by platform:

| Platform | Max File Size | Max Duration |
|----------|---------------|--------------|
| TikTok | 4 GB | 10 minutes |
| Instagram | 300 MB | 15 minutes |
| YouTube | 256 GB | 12 hours |
| LinkedIn | 5 GB | 10 minutes |
| Facebook | ~1 GB | 240 minutes |
| X (Twitter) | 1 GB+ (Premium) | 4 hours (Premium) |
| Threads | 1 GB | 5 minutes |
| Pinterest | 1 GB | 15 minutes |
| Reddit | 1 GB | 15 minutes |
| Bluesky | 100 MB | 3 minutes |

See [Video Requirements](/api/video-requirements) for detailed format specifications.

### Can I use a video URL instead of uploading the file?

Yes! Instead of uploading a file, you can pass a direct URL to your video:

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -F 'video="https://example.com/videos/myvideo.mp4"' \
  -F 'title="My Video"' \
  -F 'user="myprofile"' \
  -F 'platform[]=tiktok'
```

**Requirements for video URLs**:
- URL must be publicly accessible (test in an incognito browser)
- Should be a direct link to the file (ending in `.mp4`, `.mov`, etc.)
- Google Drive links must be set to "Anyone with the link"
- File must not be too small (< 1KB often indicates a broken link)

### Why is my video stuck in "processing"?

Videos may remain in processing status for several reasons:

1. **Long upload**: If sync upload takes > 59 seconds, it automatically switches to async processing
2. **Platform processing**: The social network is processing your video (especially for large files)
3. **Encoding issues**: The video format may need transcoding

**To check status**, use the Upload Status endpoint:
```bash
curl 'https://api.upload-post.com/api/uploadposts/status?request_id=YOUR_REQUEST_ID' \
  -H 'Authorization: Apikey YOUR_API_KEY'
```

Status values: `pending` → `in_progress` → `completed`

### How do I upload videos to TikTok drafts (MEDIA_UPLOAD mode)?

Use the `post_mode` parameter set to `MEDIA_UPLOAD`:

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -F 'video=@video.mp4' \
  -F 'title="Draft Video"' \
  -F 'user="myprofile"' \
  -F 'platform[]=tiktok' \
  -F 'post_mode="MEDIA_UPLOAD"'
```

:::info
In `MEDIA_UPLOAD` (Draft) mode, TikTok does **not** allow setting title, caption, privacy, or other metadata via API. The video uploads to your TikTok inbox/drafts, and you must add all details manually in the TikTok app before publishing.
:::

---

## Photo & Carousel Uploads

### How do I upload carousels to Instagram/TikTok/Facebook?

Upload multiple photos using the `/api/upload_photos` endpoint with the `photos[]` array:

```bash
curl -X POST https://api.upload-post.com/api/upload_photos \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -F 'photos[]=@image1.jpg' \
  -F 'photos[]=@image2.jpg' \
  -F 'photos[]=@image3.jpg' \
  -F 'title="My Carousel"' \
  -F 'user="myprofile"' \
  -F 'platform[]=instagram'
```

**Mixed carousels** (photos + videos) are supported on **Instagram and Threads only**:
```bash
-F 'photos[]=@image.jpg' \
-F 'photos[]=@video.mp4'
```

### How many photos can I include in a carousel?

| Platform | Max Photos per Post |
|----------|---------------------|
| Instagram | 10 items (photos/videos mixed) |
| Threads | 10 items (photos/videos mixed) |
| TikTok | Multiple (photo slideshow) |
| Facebook | Multiple |
| Pinterest | 5 carousel images |
| Bluesky | 4 images |
| X (Twitter) | 4 images |
| Reddit | 1 image per post |
| Discord | 10 images per message |
| Telegram | 10 photos per album |

For **X (Twitter)**, use `x_thread_image_layout` to control how images are distributed across tweets when posting more than 4 images. For **Threads**, use `threads_thread_media_layout` to control how media items are distributed across posts when posting more than 10 items.

### What image resolution/size should I use?

Recommended specifications by platform:

| Platform | Recommended Size | Max File Size | Formats |
|----------|------------------|---------------|---------|
| Instagram | 1080x1350 (4:5) | 8 MB | JPG, PNG |
| TikTok | 1080x1920 (9:16) | — | JPG, JPEG, WEBP |
| Facebook | 1200x630 | 10 MB | JPG, PNG, GIF, WebP |
| LinkedIn | 1200x627 | 8 MB | JPG, PNG, GIF |
| Pinterest | 1000x1500 (2:3) | 20 MB | JPG, PNG, GIF, WEBP |
| Threads | 1440px max width | 8 MB | JPG, PNG |
| Bluesky | — | 1 MB per image | JPG, PNG, GIF, WEBP |
| Reddit | — | 10 MB | JPG, PNG, GIF, WEBP |

See [Photo Requirements](/api/photo-requirements) for detailed specifications.

### How do I add automatic music to TikTok photos?

Use the `auto_add_music` parameter:

```bash
curl -X POST https://api.upload-post.com/api/upload_photos \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -F 'photos[]=@photo1.jpg' \
  -F 'photos[]=@photo2.jpg' \
  -F 'title="Photo slideshow with music"' \
  -F 'user="myprofile"' \
  -F 'platform[]=tiktok' \
  -F 'auto_add_music=true'
```

TikTok will automatically add background music to your photo slideshow.

---

## Scheduling

### How do I schedule a post for later?

Add the `scheduled_date` parameter (ISO-8601 format) to any upload request:

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H 'Authorization: Apikey YOUR_API_KEY' \
  -F 'video=@video.mp4' \
  -F 'title="Scheduled Post"' \
  -F 'user="myprofile"' \
  -F 'platform[]=instagram' \
  -F 'scheduled_date="2024-12-31T23:45:00Z"'
```

The API returns a `job_id` that you can use to check status, edit, or cancel the scheduled post.

**Constraints**:
- Must be in the future
- Maximum 365 days ahead

### What timezone does the API use?

By default, the API uses **UTC**. To use a different timezone, add the `timezone` parameter:

```bash
-F 'scheduled_date="2024-12-31T20:00:00"' \
-F 'timezone="America/New_York"'
```

Use any valid [IANA timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `Europe/Madrid`, `America/Los_Angeles`, `Asia/Tokyo`).

### How do I check the status of a scheduled post?

Use the Upload Status endpoint with the `job_id`:

```bash
curl 'https://api.upload-post.com/api/uploadposts/status?job_id=YOUR_JOB_ID' \
  -H 'Authorization: Apikey YOUR_API_KEY'
```

To list all scheduled posts:
```bash
curl 'https://api.upload-post.com/api/uploadposts/schedule' \
  -H 'Authorization: Apikey YOUR_API_KEY'
```

To cancel a scheduled post:
```bash
curl -X DELETE 'https://api.upload-post.com/api/uploadposts/schedule/JOB_ID' \
  -H 'Authorization: Apikey YOUR_API_KEY'
```

See [Manage Scheduled Posts](/api/schedule-posts) for full details.

---

## n8n Integration

### How do I configure credentials in n8n?

1. Add an **HTTP Request Node** to your workflow
2. Set **Method** to `POST`
3. Set **URL** to `https://api.upload-post.com/api/upload`
4. Under **Headers**, add:
   - Name: `Authorization`
   - Value: `Apikey YOUR_API_KEY`
5. Set **Body** to `multipart/form-data`
6. Add required parameters: `user`, `title`, `platform[]`

**Security tip**: Use n8n's Credentials store instead of hardcoding your API key.

### How do I pass binary files in n8n?

In the HTTP Request node, use the `formBinaryData` option:

```json
{
  "formBinaryData": {
    "video": "={{$binary.data}}"
  }
}
```

This passes the binary data from a previous node (like a Google Drive trigger) as the video file.

**Alternative**: You can pass a video URL instead of binary data:
```json
{
  "bodyParameters": {
    "parameters": [
      { "name": "video", "value": "https://example.com/video.mp4" }
    ]
  }
}
```

### Where do I find the Upload-Post n8n community node?

Currently, Upload-Post integration is done via the standard **HTTP Request node**. We provide:

- [Complete JSON node configuration](/guides/n8n-integration#complete-json-node-configuration)
- [Example workflow template on n8n.io](https://n8n.io/workflows/2894-upload-to-instagram-tiktok-and-youtube-from-google-drive/)

See our [n8n Integration Guide](/guides/n8n-integration) for step-by-step setup instructions.

---

## Common Errors

### Error: "Username required in form data"

The `user` parameter is missing from your request. This field identifies which Upload-Post profile to use.

```bash
# Correct
-F 'user="your_profile_username"'
```

### Error: "Video URL is not accessible"

The provided video URL cannot be fetched. Check that:

1. The URL is publicly accessible (test in incognito browser)
2. The URL points directly to a video file (not a webpage)
3. For Google Drive: sharing is set to "Anyone with the link"
4. The file exists and isn't too small (< 1KB indicates broken link)

### Error: "TikTok photo upload timed out"

TikTok photo uploads can be slow. Try:

1. Use `async_upload=true` to avoid timeout
2. Reduce image file sizes
3. Upload fewer photos at once
4. Check TikTok service status

### Error: "File name too long"

Some platforms reject files with very long names. Rename your file to something shorter (under 100 characters) before uploading.

### Error 502/504 Gateway Timeout

These indicate server-side timeouts, usually for large files or slow platform responses.

**Solutions**:
1. Use `async_upload=true` for large uploads
2. Reduce file size if possible
3. Retry the request
4. If persistent, contact support

### Error: "Your [Platform] session has expired"

Your social account connection has expired. Go to [Manage Users](https://app.upload-post.com/manage-users), disconnect the account, and reconnect it.

See [Common Errors](/resources/common-errors) for a complete error reference.

---

## Platform-Specific Features

### Can I add a first comment automatically?

Yes! Use the `first_comment` parameter:

```bash
-F 'first_comment="Check out the link in bio!"'
```

Supported on: **Instagram, Facebook, Threads, Bluesky, Reddit, X, YouTube, and LinkedIn**.

For platform-specific first comments, use `[platform]_first_comment`:
```bash
-F 'instagram_first_comment="Follow for more! #photography"'
-F 'youtube_first_comment="Subscribe for more videos!"'
```

### How do I upload Stories to Instagram/Facebook?

Use the `media_type` or `facebook_media_type` parameter set to `"STORIES"`:

**Instagram Stories (video)**:
```bash
-F 'platform[]=instagram' \
-F 'media_type="STORIES"'
```

**Instagram Stories (photo)**:
```bash
-F 'platform[]=instagram' \
-F 'media_type="STORIES"'
```

**Facebook Stories**:
```bash
-F 'platform[]=facebook' \
-F 'facebook_media_type="STORIES"'
```

### Can I add custom thumbnails?

Yes, for **YouTube** (standard videos only, not Shorts):

```bash
# Via URL
-F 'thumbnail_url="https://example.com/thumbnail.jpg"'

# Via file upload
-F 'thumbnail=@thumbnail.jpg;type=image/jpeg'
```

**Requirements**: JPG/PNG/GIF/BMP, max 2 MB.

### Can I add a custom cover image for Instagram Reels?

Yes, for **Instagram Reels** you can provide a cover image via URL or file upload:

```bash
# Via URL
-F 'cover_url="https://example.com/cover.jpg"'

# Via file upload (binary)
-F 'cover_image=@cover.jpg;type=image/jpeg'
```

**Requirements**: JPEG format, max 8 MB, recommended aspect ratio 9:16.

### How do I mark content as AI-generated?

For **TikTok**, use the `is_aigc` parameter:
```bash
-F 'is_aigc=true'
```

For **YouTube**, use `containsSyntheticMedia`:
```bash
-F 'containsSyntheticMedia=true'
```

### Is there an API to read/respond to comments?

Currently, Upload-Post focuses on **content publishing**. Reading and responding to comments is not supported in the API. For comment management, use each platform's native interface or their direct APIs.

---

## White-Label Integration

### How does white-label integration work?

White-label allows you to integrate Upload-Post into your own platform, so your users can connect their social accounts through your interface:

1. **Create profiles** for your users via API
2. **Generate secure URLs** (JWT) for account linking
3. **Users connect** their accounts through the Upload-Post interface (customizable branding)
4. **You manage** their content via API using their profile username

See the [White-label Integration Guide](/guides/user-profile-integration) for implementation details.

### How do I generate connection URLs for my users?

```bash
curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
  -H "Authorization: Apikey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "your_user_id_123",
    "redirect_url": "https://yourapp.com/connected",
    "logo_image": "https://yourapp.com/logo.png",
    "connect_title": "Connect Your Social Accounts",
    "platforms": ["instagram", "tiktok", "facebook"]
  }'
```

The response includes an `access_url` valid for 48 hours. Redirect your user to this URL.

### Can I use my own domain for the connection page?

Currently, the connection page is hosted at `app.upload-post.com`. You can customize:

- **Logo**: via `logo_image` parameter
- **Title**: via `connect_title` parameter
- **Description**: via `connect_description` parameter
- **Button text**: via `redirect_button_text` parameter
- **Redirect URL**: via `redirect_url` parameter
- **Visible platforms**: via `platforms` array
- **Calendar visibility**: via `show_calendar` parameter

Custom domain support is not currently available.

---

## Billing, Invoices & Payments

### How do I access my invoices?

You can access all your invoices through the **Stripe Billing Portal**:

1. Log in to your [Upload-Post Dashboard](https://app.upload-post.com/)
2. Go to **My Profile**
3. Click the **"Open Billing Portal"** button in the Invoices & Billing card at the top
4. In the Stripe Portal, click **"Invoice History"** to view and download all invoices

You can also access the Billing Portal from the profile dropdown menu in the navigation bar on any page.

### How do I update my payment method?

1. Go to **My Profile** in the dashboard
2. Click **"Open Billing Portal"**
3. In the Stripe Portal, click **"Payment methods"**
4. Add a new card or update your existing payment method

### What are the plan prices?

Visit our [Pricing page](https://www.upload-post.com/pricing) for current prices. We offer:

- **Basic** - For individuals getting started
- **Professional** - For power users and small teams
- **Advanced** - For agencies and larger teams
- **Business** - For enterprise with usage-based billing

All plans are available with monthly or yearly billing. Yearly plans include a significant discount.

### Do you charge VAT/IVA/taxes?

Upload-Post uses **Stripe** for payment processing. Depending on your country and tax regulations:

- **EU customers**: VAT may be applied automatically based on your billing address. Stripe handles EU VAT compliance.
- **Non-EU customers**: Taxes depend on your local regulations. Stripe may collect applicable taxes based on your billing address.

Your invoices in the Stripe Billing Portal will show any applied taxes. If you need a tax ID (VAT number) added to your invoices, you can add it directly in the Stripe Portal under your billing details.

### How do I cancel my subscription?

To cancel your subscription:

1. Log in to your [Upload-Post Dashboard](https://app.upload-post.com/)
2. Go to **My Profile**
3. Click **"Cancel Subscription"** and follow the steps

Your access continues until the end of the current billing period. You can also cancel directly from the Stripe Billing Portal.

### How do I request a refund?

Contact our support team at **info@upload-post.com** with:

- Your account email
- Reason for the refund request
- Any relevant details

Refund eligibility depends on your subscription terms and usage.

### Can I pause my subscription instead of canceling?

Yes! When you click "Cancel Subscription", you'll be offered the option to **pause** your subscription for 1 or 3 months instead. During the pause:

- Your data and connected accounts are preserved
- No charges during the pause period
- You can resume at any time from your profile

### How do I change my account email?

You can change your email directly from the dashboard:

1. Go to **My Profile**
2. Click **"Change email"** next to your current email
3. Enter your new email address
4. Click **"Send Verification"**
5. Check your **current email** inbox and click the confirmation link
6. Then check your **new email** inbox and click the second confirmation link
7. Your email will be updated and you'll need to log in again

This double-confirmation process protects your account from unauthorized changes.

### How do I contact human support?

- **Email**: info@upload-post.com
- **Twitter/X**: [@vcaverog](https://x.com/vcaverog)
- **GitHub**: [github.com/upload-post](https://github.com/upload-post)

When contacting support, please include:
- Your API key (masked)
- Exact error messages
- Steps to reproduce the issue
- Platform(s) affected

### Can I buy additional profiles?

Yes! You can add extra profiles as add-ons to your existing subscription:

1. Go to **My Profile** in the dashboard
2. Under Subscription, click **"Manage Extra Profiles"**
3. Choose the add-on size that fits your needs

Alternatively, you can upgrade to a higher tier plan at [upload-post.com/pricing](https://www.upload-post.com/pricing) for more base profiles.

### What happens to my data if I cancel?

When you cancel your subscription:

- Your data and connected accounts remain until the end of the billing period
- After the billing period ends, your account reverts to the free plan (limited uploads)
- Your connected social accounts and profile data are preserved
- You can resubscribe at any time to restore full access

---

## Still Have Questions?

If your question isn't answered here:

1. Check our detailed [API Reference](/api/reference)
2. Review the [Common Errors](/resources/common-errors) guide
3. Contact support at **info@upload-post.com**


---
# Pricing & Limits
URL: https://docs.upload-post.com/resources/pricing-and-limits

# Pricing & Limits

Concrete numbers for every Upload-Post plan and every enforced limit, in one place. Prices below are in USD; the live source of truth (including EUR prices and the annual-billing toggle) is [upload-post.com/#pricing](https://www.upload-post.com/#pricing).

## Plans

All paid plans include **unlimited uploads** (unlimited API upload calls per month), all supported platforms, scheduling and analytics. "Profiles" are Upload-Post sub-accounts: each profile can connect one account per platform (5 profiles = up to 5 TikTok accounts, 5 Instagram accounts, and so on).

| | Free | Basic | Professional | Advanced | Business |
|---|---|---|---|---|---|
| **Price (monthly billing)** | $0 | $24/mo | $50/mo | $147/mo | $438/mo |
| **Price (annual billing)** | $0 | $16/mo ($192/yr) | $33/mo ($400/yr) | $118/mo ($1,411/yr) | $350/mo ($4,205/yr) |
| **Uploads per month** | 10 | Unlimited | Unlimited | Unlimited | Unlimited |
| **Profiles** | 2 | 5 | 25 | 75 | 225 |
| **TikTok posting** | No | Yes | Yes | Yes | Yes |
| **Whitelabel integration** | No | No | Yes | Yes | Yes |
| **Priority support** | No | No | No | Yes | Yes |
| **Team seats** | 1 | 1 (owner only) | 2 | 5 | 10 |
| **FFmpeg video editor API** | 30 min/mo | 300 min/mo | 1,000 min/mo | 3,000 min/mo | 10,000 min/mo |
| **AI Shorts Uploader** | 10 analyses/mo | 100 analyses/mo | 300 analyses/mo | 600 analyses/mo | 1,000 analyses/mo |

Annual billing is ~40% cheaper than monthly. The Free plan requires no credit card. Check your current plan and usage with [`GET /api/uploadposts/me`](../api/current-user.md).

### Add-ons

- **Extra profiles** (choose one size, they don't stack): Basic +5 ($120/yr) or +10 ($200/yr); Professional +15 ($280/yr) or +25 ($440/yr); Advanced +25 ($650/yr) or +50 ($1,150/yr); Business: +$1/mo per extra profile.
- **[X Links add-on](../guides/x-links-addon.md):** +$19/mo. Keeps clickable URLs in X (Twitter) posts, up to 50 link-posts/mo (URLs are [stripped by default](../resources/character-limits.md#x-twitter-character-limits) to avoid X's 13× per-URL fee).

## Upload limits per plan

- **Free:** 10 uploads per month; exceeding it returns `429` with your current `usage`. TikTok uploads return `403` (paid plans only).
- **Paid plans:** unlimited monthly API upload calls. You're still subject to the per-platform daily caps below, your plan's profile count, and each network's own rules.

## Daily platform caps (per connected account, rolling 24 h)

Upload-Post enforces hard caps per connected social account to protect accounts from platform bans. They are **per account**, not per API key: 5 profiles with 5 TikTok accounts get 5 × 15 = 75 TikTok posts/day. Full details in [Limit of uploads](../guides/limit-of-uploads.md).

| Platform | Hard cap (posts / 24 h) |
|----------|------------------------:|
| Instagram | 50 |
| TikTok | 15 |
| LinkedIn | 150 |
| YouTube | 10 |
| Facebook | 25 |
| X (Twitter) | Per plan: Free/Basic 10 · Professional 20 · Advanced/Business 30 |
| Threads | 50 |
| Pinterest | 20 |
| Reddit | 40 |
| Bluesky | 50 |

Hitting a cap returns `429 Too Many Requests` with a `violations` array. Scheduled posts re-check the cap at execution time.

## API rate limits

From the [Rate Limits guide](../guides/rate-limits.md):

- Every authenticated response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers; exceeding the window returns `429`.
- **Duplicate protection:** identical uploads (same user, platform, content hash) within a short window return the existing request; send an `Idempotency-Key` header for exactly-once retries.
- **Brute-force protection:** 10 consecutive failed auth attempts block the IP for 5 minutes.
- **Polling:** don't poll [Upload Status](../api/upload-status.md) faster than every 5 s, since statuses are cached for 2 to 5 s ([recommended intervals](../guides/rate-limits.md#recommended-polling-intervals)). Or skip polling and use [webhooks](../api/webhooks.md).

## Other concrete limits worth knowing

- **Sync uploads switch to async after 59 s.** Use [`async_upload=true`](../guides/async-uploads.md) and poll with the returned `request_id`.
- **Scheduling:** `scheduled_date` must be in the future, at most 365 days ahead ([Schedule posts](../api/schedule-posts.md)).
- **White-label JWT connect URLs** expire after 48 hours ([User Profiles API](../api/user-profiles.md)).
- **AutoDM monitors:** 2 per profile per day, auto-expire after 15 days ([AutoDM API](../api/autodms.md)).
- **Per-platform media caps** (file size, duration, carousel items): see [Video Requirements](../api/video-requirements.md), [Photo Requirements](../api/photo-requirements.md) and [Character Limits](./character-limits.md).


---
# Support
URL: https://docs.upload-post.com/resources/support

# Support

We're here to help you succeed with the Upload-Post API. Here are the different ways you can get support:

## Contact Options

### Email Support
- **Support**: info@upload-post.com

### Community Support
- Follow us on [Twitter](https://x.com/vcaverog)
- Check our [GitHub repository](https://github.com/upload-post)

## Getting Help

### Before Contacting Support
1. Check our [documentation](../introduction)
2. Review our [FAQ](./faq)
3. Search for similar issues in our [GitHub issues](https://github.com/upload-post)

### When Contacting Support
Please include:
- Your API key (masked)
- Error messages or logs
- Steps to reproduce the issue
- Expected vs actual behavior
- Any relevant code snippets

## Bug Reports

If you've found a bug:
1. Check if it's already reported on [GitHub](https://github.com/upload-post)
2. Create a new issue with:
   - Clear description
   - Steps to reproduce
   - Expected behavior
   - Actual behavior
   - Environment details

## Feature Requests

We welcome feature requests! Submit them through:
- [GitHub issues](https://github.com/upload-post)
- Email to info@upload-post.com


---
# SDK Examples
URL: https://docs.upload-post.com/sdk-examples

# SDK Examples

Explore real-world examples using the Upload Post SDK in Python and JavaScript.

[![PyPI version](https://badge.fury.io/py/upload-post.svg)](https://badge.fury.io/py/upload-post)
[![npm version](https://badge.fury.io/js/upload-post.svg)](https://badge.fury.io/js/upload-post)

## cURL

### Upload Video

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'video=@/path/to/your/video.mp4' \
  -F 'title="Your Video Title"' \
  -F 'user="test"' \
  -F 'platform[]=tiktok' \
  -X POST https://api.upload-post.com/api/upload
```

### Upload Photos

```bash
curl \
  -H 'Authorization: Apikey your-api-key-here' \
  -F 'photos[]=@/path/to/your/image1.jpg' \
  -F 'user="test"' \
  -F 'platform[]=instagram' \
  -F 'title="My Photo Title"' \
  -F 'description="My photo description"' \
  -X POST https://api.upload-post.com/api/upload
```

## Python

### Basic Upload

```python
from upload_post import UploadPostClient

client = UploadPostClient(api_key="your-api-key-here")

# Upload video to multiple platforms
response = client.upload_video(
    video_path="/path/to/video.mp4",
    title="My Awesome Video",
    user="testuser",
    platforms=["tiktok", "instagram"]
)

print('Upload successful:', response)
```

## JavaScript/Node.js

### Basic Upload

```javascript

const uploader = new UploadPost('your-api-key-here');

// Upload video with options
const result = await uploader.upload('/path/to/video.mp4', {
  title: 'My Awesome Video',
  user: 'test-user',
  platforms: ['tiktok'] // Currently supported platforms
});

console.log('Upload successful:', result);
```
