# Migrate from Upload-Post to the OmniSocials API

> Index of all OmniSocials docs pages: https://docs.omnisocials.com/llms.txt

This guide is for developers who post through the Upload-Post API and want to move to the OmniSocials API. Upload-Post has a separate multipart endpoint for each content type and picks the profile with a `user` field. OmniSocials has one JSON endpoint for every post type, and the API key itself picks the workspace. Media goes in as a URL or as an ID from an earlier upload. Social accounts are connected in the OmniSocials dashboard, not through a connect URL. Upload-Post facts on this page come from docs.upload-post.com and were checked on October 21, 2026.

## Concepts

| Upload-Post | OmniSocials | Notes |
|-------------|-------------|-------|
| API key (`Authorization: Apikey ...`) | API key (`Authorization: Bearer omsk_live_...`) | An OmniSocials key belongs to one workspace. |
| Profile (the `user` field, a username) | Workspace | Each workspace has its own API key. There is no `user` field. |
| Social accounts on a profile | Accounts in a workspace | One account per channel ID per workspace. |
| Connect URL (`generate-jwt`, `oauth/{platform}/start`) | Dashboard connection | Accounts are connected only in the dashboard, never through the API. |
| `request_id` and `job_id` | Post `id` | One ID for drafts, scheduled posts, and published posts. |
| Separate upload endpoints for video, photos, text, and documents | `POST /posts/create` | One endpoint. `type` picks a feed post, story, or reel. |
| Facebook page, LinkedIn page, and Google Business location IDs per request | Chosen when the account is connected | A workspace posts to one Facebook Page, one LinkedIn page, and one Google Business location. |
| Queue | None | `GET /analytics/best-times` recommends slots. You pick the time. |

## Authentication

```text
Upload-Post: Authorization: Apikey YOUR_API_KEY
OmniSocials: Authorization: Bearer omsk_live_...
```

With Upload-Post, one key serves every profile, and each request names the profile in `user`. An OmniSocials key is bound to one workspace, chosen when you create the key under **Settings → API**. To serve several brands, create one workspace and one key per brand, and store the key where you stored the Upload-Post username. `GET /accounts` returns `workspace_id` and `workspace_name`, so you can check which workspace a key belongs to.

Keys have scopes, such as `posts:write` and `media:write`. A key's scopes cannot be edited later. See [Authentication](https://docs.omnisocials.com/authentication.md).

## Endpoint mapping

OmniSocials paths are relative to `https://api.omnisocials.com/v1`. Upload-Post paths are relative to `https://api.upload-post.com`.

| Upload-Post | OmniSocials | Notes |
|-------------|-------------|-------|
| `POST /api/upload` (video) | `POST /posts/create` | Pass the video in `media_urls` or `media_ids`. Use `type: "reel"` for TikTok videos, Instagram reels, and YouTube Shorts. |
| `POST /api/upload_photos` | `POST /posts/create` | Images in `media_urls` or `media_ids`. Two or more images publish as a carousel where the platform supports it. |
| `POST /api/upload_text` | `POST /posts/create` | No media. Instagram, TikTok, Pinterest, and YouTube need media. |
| `POST /api/upload_document` | `POST /posts/create` | Pass a PDF. On LinkedIn it publishes as a document. PPT and DOC files are not accepted. |
| `GET /api/uploadposts/status` | `GET /posts/{id}` | Read `status`, `published_urls`, and `errors`. |
| `GET /api/uploadposts/schedule` | `GET /posts?status=scheduled` | Page with `limit` (max 100) and `offset`. |
| `GET /api/uploadposts/schedule/<job_id>` | `GET /posts/{id}` | |
| `PATCH /api/uploadposts/schedule/<job_id>` | `PATCH /posts/{id}` | Drafts and scheduled posts only. Media and channels can change too. |
| `DELETE /api/uploadposts/schedule/<job_id>` | `DELETE /posts/{id}` | Cancels a scheduled publish. On a published post it removes the post from OmniSocials only. |
| `GET /api/uploadposts/history` | `GET /posts?status=published` | `GET /posts/recent-platform` reads recent posts made outside OmniSocials. |
| `GET /api/analytics/{profile_username}` | `GET /analytics/accounts`, `GET /analytics/overview` | |
| `GET /api/uploadposts/post-analytics/{request_id}` | `GET /analytics/posts/{id}` | `GET /analytics/posts?ids=` takes up to 100 post IDs. |
| `GET /api/uploadposts/me` | `GET /accounts` | Returns the workspace and its connected accounts. |
| `GET /api/uploadposts/pinterest/boards` | `GET /accounts/{id}` | A Pinterest account carries a `boards` array. Boards cannot be created through the API. |
| `GET /api/uploadposts/facebook/pages`, `GET /api/uploadposts/linkedin/pages` | None | The page is chosen when the account is connected. |
| `POST /api/uploadposts/users` and profile CRUD | None | Manage workspaces in the dashboard. |
| `POST /api/uploadposts/users/generate-jwt`, `POST /api/uploadposts/oauth/{platform}/start` | None | Connect accounts in the dashboard. |
| `DELETE /api/uploadposts/users/social` | None | Disconnect accounts in the dashboard. |
| `/api/uploadposts/queue/*` | None | |
| Notification settings | `POST /webhooks`, `GET`, `PATCH`, `DELETE /webhooks/{id}` | See [Webhooks](#webhooks). |

## Create a post

The same post in both APIs: one video to TikTok and YouTube, scheduled for 08:00 UTC on November 2, 2026.

Upload-Post sends the file and the fields in one multipart request:

```bash
curl -X POST https://api.upload-post.com/api/upload \
  -H "Authorization: Apikey $UPLOAD_POST_API_KEY" \
  -F "user=acme" \
  -F "platform[]=tiktok" \
  -F "platform[]=youtube" \
  -F "video=@/path/to/launch.mp4" \
  -F "title=Launch day behind the scenes" \
  -F "scheduled_date=2026-11-02T08:00:00Z"
```

OmniSocials takes JSON. Upload the file first, then create the post with the returned ID:

```bash
curl -X POST https://api.omnisocials.com/v1/media/upload \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -F "file=@/path/to/launch.mp4"

curl -X POST https://api.omnisocials.com/v1/posts/create \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "reel",
    "content": "Launch day behind the scenes",
    "accounts": ["tiktok", "youtube"],
    "media_ids": ["123"],
    "youtube": { "title": "Launch day behind the scenes" },
    "schedule_at": "2026-11-02T08:00:00Z"
  }'
```

Replace `123` with `data.id` from the upload response. If the video is already at a public URL, skip the upload and pass `"media_urls": ["https://example.com/launch.mp4"]` instead. On YouTube, `content` becomes the video description and `youtube.title` the title.

The create call answers with a `201` and the post under `data`. Publishing always runs in the background. Read the outcome with `GET /posts/{id}` or a `post.published` webhook.

## Field mapping

| Upload-Post field | OmniSocials field | Notes |
|-------------------|-------------------|-------|
| `user` | None | The API key selects the workspace. |
| `platform[]` | `accounts` | A JSON array. `x` and `google_business` keep their names. Alias: `channels`. |
| `video` | `media_urls` or `media_ids` | A URL, or the ID of an earlier upload. The file itself is not sent in the post request. |
| `photos[]` | `media_urls` or `media_ids` | Up to 10 URLs per post. |
| `document` | `media_urls` or `media_ids` with a PDF | One image per page. A LinkedIn document on LinkedIn. |
| `title` | `content` | The post text. A string, or an object with `default` and channel keys. |
| `[platform]_title`, such as `x_title` | `content.x`, `content.linkedin`, and so on | Per-channel text. On YouTube, set the title in `youtube.title`. |
| `description` | `content` | On YouTube, `content` becomes the description. |
| `scheduled_date` | `schedule_at` | ISO 8601 in UTC. Alias: `scheduled_at`. |
| `timezone` | None | Convert to UTC before you send. |
| `first_comment`, `[platform]_first_comment` | `first_comment` under the channel key, such as `instagram.first_comment` | Instagram, Facebook, LinkedIn, YouTube, TikTok, and Telegram. |
| `privacy_level` | `tiktok.privacy_level`, `youtube.privacy_status` | |
| `pinterest_board_id` | `pinterest.board_id` | Required on every Pinterest post. |
| `facebook_page_id` | None | The Page is chosen when Facebook is connected. |
| `target_linkedin_page_id` | `linkedin_page` in `accounts` | `linkedin` is the personal profile. |
| `gbp_location_id` | None | The location is chosen when Google Business is connected. |
| `add_to_queue`, `max_posts_per_slot` | None | |
| `async_upload` | None | Publishing is always asynchronous. |
| `external_id`, `request_id`, `Idempotency-Key` | None | Store the returned post `id` next to your own ID. |

## Media and scheduling

**Media.** Upload-Post takes the file inside the post request. OmniSocials keeps media and posts apart:

- **`media_urls` on the post.** Pass public URLs and OmniSocials downloads them. Up to 10 files, each up to 100 MB.
- **`POST /media/upload`.** A multipart upload with a `file` field, up to 100 MB. Use `data.id` in `media_ids`.
- **`POST /media/upload-from-url`.** OmniSocials fetches a public URL of up to 1 GB. Files above about 100 MB come back with `status: "processing"`. Poll `GET /media/{id}` until it is `ready`.
- **`POST /media/upload-from-base64`.** For a client that holds the file as base64.

A file in the library can be used on many posts, so upload once when the same video goes out more than once. The largest file OmniSocials takes is 1 GB. Platform limits, such as video length, are checked when a post is scheduled or published. See [Media](https://docs.omnisocials.com/media.md).

**Scheduling.** Send `schedule_at` as an ISO 8601 time in UTC. OmniSocials publishes within one minute of that time. There is no `timezone` field and no queue. A time in the past returns `400 validation_error`. To publish now, send `publish_now: true`, or call `POST /posts/create-and-publish`. A post with neither `schedule_at` nor `publish_now` is saved as a draft.

## Webhooks

Register a webhook with `POST /webhooks`. Webhooks belong to the workspace of the key that creates them. The response returns a `secret` once. Store it.

```bash
curl -X POST https://api.omnisocials.com/v1/webhooks \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/omnisocials",
    "events": ["post.published", "post.failed"]
  }'
```

| Upload-Post event | OmniSocials event | Notes |
|-------------------|-------------------|-------|
| `upload_completed` | `post.published`, `post.failed` | `post.published` fires when at least one channel succeeds. A partial publish has `data.status` set to `warning`. Check each entry in `data.targets`. |
| `social_account_connected`, `social_account_disconnected`, `social_account_reauth_required` | None | Poll `GET /accounts` and check `needs_reconnect`. |
| None | `post.scheduled` | Sent when a post is scheduled through the API. |
| None | `post.approved`, `post.rejected` | Approval workflow outcomes. |

**Signatures.** Both APIs sign the same string: the timestamp, a dot, and the raw body, with HMAC-SHA256 and your webhook secret. Only the headers change. Upload-Post sends `X-Upload-Post-Signature: sha256=<hex>` and `X-Upload-Post-Timestamp`. OmniSocials sends one header, `X-OmniSocials-Signature: t=<timestamp>,v1=<hex>`. Deduplicate on `X-OmniSocials-Delivery`. See [Webhooks](https://docs.omnisocials.com/webhooks.md#verifying-signatures) for verification code.

**Delivery.** Answer with a `2xx` within 10 seconds. Failed deliveries are retried twice, after 2 seconds and then 5 more. After that the event is dropped. Webhook URLs must use HTTPS.

## Platform coverage

As of October 21, 2026, from docs.upload-post.com and the [OmniSocials platforms page](https://docs.omnisocials.com/platforms.md).

| Platform | Upload-Post | OmniSocials |
|----------|-------------|-------------|
| Facebook | Yes (`facebook`) | Yes (`facebook`), Pages |
| Instagram | Yes (`instagram`), no text-only posts | Yes (`instagram`), Business and Creator accounts |
| Threads | Yes (`threads`) | Yes (`threads`) |
| LinkedIn | Yes (`linkedin`), profiles and pages | Yes, profile (`linkedin`) and company page (`linkedin_page`) |
| YouTube | Yes (`youtube`) | Shorts only (`youtube`) |
| TikTok | Yes (`tiktok`) | Yes (`tiktok`) |
| Pinterest | Yes (`pinterest`) | Yes (`pinterest`) |
| Bluesky | Yes (`bluesky`) | Yes (`bluesky`) |
| X | Yes (`x`) | Yes (`x`) |
| Google Business Profile | Yes (`google_business`) | Yes (`google_business`) |
| Telegram | Yes (`telegram`) | Yes (`telegram`), a channel or group |
| Mastodon | Yes (`mastodon`) | Yes (`mastodon`) |
| Reddit | Listed, currently unavailable | Coming soon |
| Snapchat | Not listed | Coming soon |
| Discord | Yes (`discord`) | No |
| WordPress | Yes (`wordpress`) | No |
| Slack, Nostr, Lemmy, Dev.to, Hashnode, Whop, Listmonk | Text posts | No |

## What is different

- **Account connection.** OmniSocials has no `generate-jwt` or OAuth start endpoint. Accounts are connected in the dashboard. If your product lets end users connect their own accounts through your app, OmniSocials does not support that today.
- **No profiles API.** Workspaces are created in the dashboard, and each API key reaches one workspace.
- **One account per channel.** A workspace posts to one account per channel ID, including one Facebook Page, one LinkedIn page, and one Google Business location. Pages cannot be picked per request.
- **JSON, not multipart.** Posts are JSON. Media is a URL or an ID from an earlier upload.
- **File size.** The largest file OmniSocials takes is 1 GB, through `upload-from-url`. Direct uploads and `media_urls` stop at 100 MB per file.
- **YouTube is Shorts only**, up to 3 minutes. Documents must be PDF.
- **Rate limit.** OmniSocials allows 100 requests per minute per key. Upload-Post documents 60 to 1,000 requests per minute, by plan and number of profiles. Limits are per key on OmniSocials, so you can split heavy work across keys. See [Rate limits and errors](https://docs.omnisocials.com/rate-limits-and-errors.md).
- **No queue, timezone field, or idempotency key.**
- **X links.** X posts whose text contains a link use prepaid credits. Posts without a link do not. See [X](https://docs.omnisocials.com/platforms/x.md).
- **Fewer webhook events.** There are no account events.
- **Error shape.** Upload-Post errors use `success`, `message`, and `error_code`. OmniSocials errors nest under `error`, with `code`, `message`, and optional `details`. Branch on `error.code`.
- **Fewer channels.** Discord, WordPress, Slack, and the other text-only channels are not part of OmniSocials. Reddit is not live yet.
- **No test mode.** Neither API documents one. On OmniSocials, use drafts or a workspace with test accounts.
- **What OmniSocials adds.** A [Social inbox](https://docs.omnisocials.com/inbox.md) API for DMs and comments, approval workflows with `post.approved` and `post.rejected` events, alt text per image, PDF to carousel on Instagram, TikTok, Threads, and Pinterest, hashtag sets, and Instagram comment-to-DM automations.

## Migration steps

1. Create an OmniSocials account. Add one workspace for each Upload-Post profile.
2. Connect each social account in the dashboard, under **Settings → Organisation → Workspaces**. Pick the Facebook Page, LinkedIn page, and Google Business location there.
3. Create one API key per workspace under **Settings → API**. Start with `posts:write`, `posts:read`, `media:write`, `accounts:read`, and `webhooks:manage`.
4. Replace each Upload-Post username in your config with that workspace's key.
5. Call `GET /accounts` with each key to see the connected channels. For Pinterest, copy the board IDs from `GET /accounts/{id}`.
6. Replace the four upload calls with one `POST /posts/create`. Send JSON. Move files to `media_urls`, or upload them first and pass `media_ids`. Rename the fields with the [field mapping](#field-mapping) table.
7. Convert `scheduled_date` and `timezone` to a UTC `schedule_at`.
8. Register your webhooks with `POST /webhooks`. Swap the signature check and map the events.
9. Recreate the posts that are still scheduled in Upload-Post with `schedule_at`. Then cancel them with `DELETE /api/uploadposts/schedule/<job_id>`, so nothing publishes twice.
10. Test with drafts first, since there is no test mode. Then publish one post with `publish_now: true` and check `published_urls` on `GET /posts/{id}`.

:::tip
Posts without `schedule_at` or `publish_now` are drafts and never publish on their own. Use them to check your field mapping against real accounts.
:::
