API Documentation
Authenticate with an API key, list your connected social accounts, and create, schedule and publish posts programmatically.
Overview
The Postfeeds API is a resource-oriented REST API. All requests and responses are JSON.
Authentication
Every request needs an API key sent as a Bearer token. Create one at Dashboard → API Keys — the full key is shown once (only a hash is stored) and can be revoked instantly.
curl https://postfeeds.com/api/v1/accounts \
-H "Authorization: Bearer pf_live_..."Keys carry scopes (all granted by default): read for GETs, posts:write to create/cancel posts, profiles:write to create profiles.
Quickstart
- Create an API key at
Dashboard → API Keys. - Connect a TikTok account at
Dashboard → Connections(one-time, in the browser — the OAuth flow needs a signed-in session, so accounts can't be connected via the API). GET /accounts→ copy the accountid.POST /postswithpublishNow: true.- Poll
GET /posts/{id}for delivery status.
# 1. Which accounts can I post to?
curl https://postfeeds.com/api/v1/accounts \
-H "Authorization: Bearer $PF_API_KEY"
# 2. Publish a video to one of them
curl -X POST https://postfeeds.com/api/v1/posts \
-H "Authorization: Bearer $PF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "New drop",
"media": ["https://cdn.example.com/clip.mp4"],
"platforms": [{ "platform": "tiktok", "accountId": "<account id>" }],
"publishNow": true
}'Core concepts
| Concept | What it is |
|---|---|
| Account | A connected social account. Its id is the accountId you post to. |
| Profile | A named group of accounts — a brand, client or campaign. |
| Post | One piece of content for one account. A single POST /posts fans out to many accounts and returns one post per account. |
Post modes
| You send | What happens |
|---|---|
publishNow: true | Delivered to the platform immediately. |
scheduledFor: ISO-8601 | Stored, then delivered automatically when due. |
| neither | Saved as a draft — media persisted, nothing sent. |
Post lifecycle
draft → scheduled → uploading → processing → failed | canceled. processingmeans the content was handed to the platform — for TikTok it's now in the user's TikTok app inbox (drafts), where they finish publishing. Failed posts carry an error message.
Media rules
Media is passed by URL— the server downloads it. URLs must be public http(s). If your files aren't hosted anywhere, upload them first with POST /media/presign and use the returned publicUrl. Per post, exactly one of:
| Kind | Formats | Limits |
|---|---|---|
| Video | .mp4 .mov .webm | One per post, max 200 MB |
| Photos | .jpg .png .webp | 1–35 per post, max 20 MB each |
Photos are auto-resized to TikTok's 1080×1920 canvas, re-encoded as JPEG and rehosted on Postfeeds' TikTok-verified storage — your image host does not need TikTok URL verification.
Endpoints
/accountsList connected social accounts. Use an account's id as accountId when posting.
{
"accounts": [
{
"id": "adee4cee-399a-480e-ab8d-274fcc31d794",
"platform": "tiktok",
"handle": "post.feeds",
"displayName": "Postfeeds",
"avatarUrl": "https://...",
"status": "active",
"profileId": "98437905-fa96-4f2d-b5bf-60071c52a6e7",
"connectedAt": "2026-06-06T16:39:34+00:00"
}
]
}/profilesList profiles (groups of accounts).
{
"profiles": [
{ "id": "...", "name": "Default Profile", "color": "violet",
"isDefault": true, "createdAt": "2026-06-05T12:41:32+00:00" }
]
}/profilesCreate a profile. Body: { "name": "Client — Acme", "color": "violet" } (color optional). Returns 201 with the profile.
/postsCreate, schedule or publish a post to one or more accounts. Supports the Idempotency-Key header — retries with the same key return the original response and never duplicate the post.
| Field | Type | Required | Notes |
|---|---|---|---|
content | string | yes | Caption, max 4000 chars — 2200 for video (may be empty) |
title | string | no | Video title |
media | string[] | yes | Public media URLs — see Media rules |
platforms | object[] | yes | One { platform, accountId } per target |
scheduledFor | ISO-8601 | no | Future date — schedule for later |
publishNow | boolean | no | Mutually exclusive with scheduledFor |
tiktokSettings | object | no | { draft: false, privacyLevel } directly publishes TikTok photos |
curl -X POST https://postfeeds.com/api/v1/posts \
-H "Authorization: Bearer $PF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 1f6d2c3e-8a4b-4f0e-9d1a-aaaa00000001" \
-d '{
"content": "New drop",
"media": ["https://cdn.example.com/clip.mp4"],
"platforms": [
{ "platform": "tiktok", "accountId": "adee4cee-..." }
],
"publishNow": true
}'For a direct TikTok photo or carousel post, use publishNow: true and include tiktokSettings with draft: false and an allowed privacyLevel. Omitting these settings preserves the TikTok drafts flow.
{
"content": "Six ideas for a better launch",
"media": [
"https://cdn.example.com/slide-1.png",
"https://cdn.example.com/slide-2.png"
],
"platforms": [
{ "platform": "tiktok", "accountId": "adee4cee-..." }
],
"publishNow": true,
"tiktokSettings": {
"draft": false,
"privacyLevel": "SELF_ONLY"
}
}{
"posts": [
{
"id": "a4a4ab72-5183-46af-8c77-2785ce2572ec",
"accountId": "adee4cee-399a-480e-ab8d-274fcc31d794",
"platform": "tiktok",
"status": "processing",
"publishId": "v_inbox_file~v2.123456789"
}
]
}Per-item status is processing (publishNow), scheduled, draft or failed (with error). One failed item doesn't fail the whole request.
/postsList posts, newest first. Query params: status, platform, limit (default 20, max 100), offset. Returns { posts, limit, offset }.
/posts/{id}A single post and its delivery status.
{
"id": "a4a4ab72-...",
"accountId": "adee4cee-...",
"platform": "tiktok",
"title": "",
"content": "New drop",
"status": "processing",
"scheduledFor": null,
"publishId": "v_inbox_file~v2.123456789",
"error": null,
"createdAt": "2026-06-11T08:29:16+00:00"
}/posts/{id}Cancel a draft or scheduled post (anything else → 400). Stored media is removed. Returns { "id": "...", "status": "canceled" }.
/media/presignDirect upload for files that aren't hosted at a public URL. Returns a presigned PUT URL (valid 15 minutes) and the public URL the file will have. PUT the raw bytes to uploadUrl with the same Content-Type, then use publicUrl in the media array of POST /posts. Accepts contentType (required) and size in bytes (recommended — enforced on upload when provided); same format and size limits as Media rules.
{
"uploadUrl": "https://...r2.cloudflarestorage.com/...&X-Amz-Signature=...",
"publicUrl": "https://media.postfeeds.com/<userId>/uploads/<uuid>.jpg",
"key": "<userId>/uploads/<uuid>.jpg",
"expiresIn": 900
}/usageQuota and rate-limit picture for the calling key.
{
"postsThisMonth": 8,
"connections": { "used": 2, "max": 2 },
"rateLimit": { "perMinute": 60 }
}Errors
Every error has the same envelope and a meaningful HTTP status:
{ "error": { "code": "invalid_request", "message": "media must be an array of URLs" } }| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed body or params |
| 401 | unauthorized | Missing, invalid or revoked API key |
| 403 | forbidden | Key lacks the required scope |
| 404 | not_found | Resource doesn't exist or isn't yours |
| 422 | unsupported | e.g. platform not yet supported |
| 429 | rate_limited | Rate limit exceeded |
| 502 | upstream_error | The social platform rejected the request |
| 500 | internal_error | Unexpected server error |
Rate limits & idempotency
Default: 60 requests/minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; exceeding the limit returns 429 with Retry-After: 60.
POST /posts accepts an Idempotency-Key header (any unique string). Replays return the original response with Idempotency-Replayed: true — safe retries, no double-posting.
Platform support
The API currently delivers to TikTok and LinkedIn. Other platforms return 422 unsupported and are coming as their backends ship.
- TikTok— video posts use the inbox/draft flow: the video lands in the connected user's TikTok app inbox and they tap to finish publishing — the right flow while a TikTok app's Content Posting audit is pending. Photos land as drafts via MEDIA_UPLOAD. Media is always required.
- LinkedIn— posts publish straight to the member's feed (status
published, post URN inpublishId). Text-only or 1–35 images; video not yet supported. Tokens last ~60 days without refresh — expired accounts showexpiredand need a reconnect. - Accounts are connected in the dashboard (
Dashboard → Connections), not via the API — the OAuth flow requires a signed-in browser session.
Migrating from Zernio
If your client currently targets Zernio's API:
| Zernio | Postfeeds |
|---|---|
zernio.com/api/v1 | postfeeds.com/api/v1 |
sk_... | pf_live_... |
GET /accounts → _id | GET /accounts → id |
POST /posts → { post: { _id } } | { posts: [{ id }] } (array) |
mediaItems | media — plain array of URL strings |
tiktokSettings / draft: true | Omit publishNow + scheduledFor for a draft; TikTok settings handled server-side |
POST /media/presign | POST /media/presign — same flow: presigned PUT, then use publicUrl |
GET /connect/tiktok | Not available — connect accounts in the dashboard |
POST /tools/validate/post | Not available — POST /posts returns precise 400s |
GET /logs | Use GET /posts/{id} (status + error) |
Zernio account/profile IDs are meaningless here — re-fetch them from GET /accounts and GET /profiles after connecting your accounts.