Create Post

View as Markdown

Schedule or immediately queue a post across multiple social media platforms.

Endpoint

POST https://api.publora.com/api/v1/create-post

Headers

Header Required Description
x-publora-key Yes Your API key
x-publora-user-id No Managed user ID (workspace only)
x-publora-client No Client identifier (e.g., mcp for MCP integrations). Affects which access controls are checked.
Idempotency-Key No Opt-in duplicate protection. A client-generated unique string. Retrying with the same key and same body replays the original response instead of creating a second post. See Idempotency.
Content-Type Yes application/json

Browser usage: CORS permits x-publora-key, x-publora-user-id, x-publora-client, and Idempotency-Key, but only requests from origins in Publora's deployment allowlist are accepted. An arbitrary integrator origin can still fail preflight. Even from an allowed origin, embedding a secret API key in client-side code exposes it; prefer a server-side proxy.

Request Body

Parameter Type Required Description
content string Conditional Normally required and non-empty. It may be omitted or empty when a targeted LinkedIn connection has repost intent through platformSettings.linkedin.repostEnabled: true or a non-empty repostParentUrn; the repost settings must still form a valid parent/enable combination. Non-string truthy values (numbers, objects) can pass the initial content gate but cause unexpected behavior.
platforms string[] Yes Array of <lowercase-prefix>-<id> connection IDs. The ID cannot contain whitespace, /, ?, or #; colons are allowed (e.g., twitter-123456789, bluesky-did:plc:abc123).
scheduledTime string No ISO 8601 UTC datetime. If omitted, the post is created as a draft. A time in the past is never silently accepted — it is either clamped to server time with a SCHEDULED_TIME_COERCED warning in the response, or rejected with 400 SCHEDULED_TIME_IN_PAST. See Past scheduled times.
platformSettings object No Per-platform settings that are merged with server-side defaults. User-provided values override defaults on a per-platform basis. The accepted keys are tiktok, instagram, youtube, threads, telegram, and linkedin. Any unknown top-level platform or unknown nested key is rejected with 400 PLATFORM_SETTING_UNKNOWN and nothing is persisted — see Unknown platformSettings paths for the exact accepted tree. Each platform key must map to a plain object. Validation errors ("Invalid platformSettings JSON", "platformSettings must be an object") are returned if the field is present and malformed.
mediaUrls string[] No Up to 10 public https image/video URLs. Publora downloads them server-side and attaches them to the post before validation, so you can attach media and schedule in one call (pass together with scheduledTime). This is the one-shot alternative to the draft → get-upload-url → schedule flow. Ingestion is rate-limited to 60 URLs/hour. See Posts with Media.

Response

Returns HTTP 200 on success (not 201).

{
  "success": true,
  "postGroupId": "507f1f77bcf86cd799439011",
  "scheduledTime": "2026-03-01T14:00:00.000Z"
}

Use the postGroupId to track, update, or delete the post.

Field Always present Description
success Yes true on a 200
postGroupId Yes Post group ID — use it to track, update, or delete the post
scheduledTime Yes The effective scheduled time actually stored (ISO 8601 UTC), or null for a draft. Trust this over the value you sent — a permitted past-time clamp may change it; a plan-horizon violation is rejected rather than adjusted.
warnings No Present only when the request was accepted with a caveat. Array of { code, message, requested, effective }.
media No Per-URL ingestion results, present only when mediaUrls was supplied

When a past scheduledTime is clamped, the response carries a warning and scheduledTime reflects the clamped value:

{
  "success": true,
  "postGroupId": "507f1f77bcf86cd799439011",
  "scheduledTime": "2026-03-01T14:03:11.000Z",
  "warnings": [
    {
      "code": "SCHEDULED_TIME_COERCED",
      "message": "Requested scheduled time 2026-03-01T14:01:00.000Z was in the past and was changed to server time 2026-03-01T14:03:11.000Z.",
      "requested": "2026-03-01T14:01:00.000Z",
      "effective": "2026-03-01T14:03:11.000Z"
    }
  ]
}

Past scheduled times

A scheduledTime in the past is handled by how far in the past it is. The 5-minute tolerance absorbs ordinary client clock skew; anything beyond it is a real bug in the caller's logic.

How far in the past Behaviour today Behaviour when strict mode is active (scheduled 2026-08-25 by default)
Less than 5 minutes Clamped to server time + SCHEDULED_TIME_COERCED warning Unchanged — clamped + warned
5 minutes or more Clamped to server time + SCHEDULED_TIME_COERCED warning Rejected with 400 / SCHEDULED_TIME_IN_PAST

⚠️ Scheduled strict date — 2026-08-25. Sending a scheduledTime 5+ minutes in the past currently succeeds with a warning. It is scheduled to become a hard 400 on 2026-08-25, unless production configuration overrides that date either way. If you see SCHEDULED_TIME_COERCED in your responses today, fix it before that date: the usual causes are an unsynced client clock, a local-time value sent without a UTC offset, or replaying a stored scheduledTime from an earlier run. Compare your requested time against the serverTime / effective values the API returns.

The 5-minute tolerance is permanent. The sunset changes the handling of times 5+ minutes in the past and nothing else. A time 2 minutes in the past will still be clamped-and-warned after 2026-08-25, exactly as it is today.

The rejection body:

{
  "error": "Scheduled time is in the past. Server time is 2026-03-01T14:03:11.000Z UTC.",
  "code": "SCHEDULED_TIME_IN_PAST",
  "serverTime": "2026-03-01T14:03:11.000Z"
}

Use serverTime to measure your clock offset against Publora's.

Unknown platformSettings paths

platformSettings is validated against a strict allowlist before anything is persisted. An unrecognized top-level platform (e.g. twitter, facebook) or an unrecognized nested key returns 400 and creates no post:

{
  "error": "Unknown platformSettings path: youtube.thumbnail.mediaID",
  "code": "PLATFORM_SETTING_UNKNOWN",
  "field": "youtube.thumbnail.mediaID"
}

Why this matters: a typo like youtube.thumbnail.mediaID (capital D) or instagram.coverurl previously passed silently and wiped the corresponding setting. It is now a loud 400 naming the exact field.

Nested reference objects are checked to their leaves — youtube.playlist and youtube.thumbnail cannot carry extra keys. The complete accepted tree:

Platform Accepted keys
tiktok viewerSetting, allowComments, allowDuet, allowStitch, commercialContent, brandOrganic, brandedContent
instagram videoType, shareToFeed, coverUrl (alias cover_url)
youtube privacy, title, tags, madeForKids, categoryId, playlist.{id,platformId}, thumbnail.{mediaId,id,url,path}
threads replyControl
telegram disableNotification, disableWebPagePreview, protectContent
linkedin repostEnabled, repostParentUrn, repostVisibility

LinkedIn repost settings default to false, an empty parent URN, and an empty visibility. When enabled, use a canonical urn:li:share:... or urn:li:ugcPost:... parent. CONNECTIONS visibility is supported only for personal connections; scheduling a company-page repost with it returns 400 with "LinkedIn company-page reposts cannot use CONNECTIONS visibility; choose PUBLIC". See LinkedIn Repost Settings for the full contract (commentary, media rules, error strings).

Still accepted, unchanged:

  • REST aliasesinstagram.cover_url, youtube.thumbnail.id (for mediaId), youtube.thumbnail.path (for url).
  • String booleans"false" / "0" / "off" coerce to false on telegram and tiktok boolean keys.
  • Comma-separated youtube.tags — a string is split into an array.
  • Legacy flat fieldsyoutube.playlistId and youtube.thumbnailUrl are not PLATFORM_SETTING_UNKNOWN; they keep returning their existing migration-hint 400 (e.g. "playlistId is not supported; use …playlist"). They are never persisted.

Idempotency

Send an optional Idempotency-Key request header to make create-post safe to retry. This solves the classic agent failure: the request times out or the connection drops, the client retries, and the user ends up with two identical posts. With a key, the retry replays the original response — one post, not two.

curl -X POST https://api.publora.com/api/v1/create-post \
  -H "Content-Type: application/json" \
  -H "x-publora-key: YOUR_API_KEY" \
  -H "Idempotency-Key: 8f1c0b6e-6a2f-4a1e-9f3b-2b1d0c9a7e55" \
  -d '{
    "content": "Excited to share our new product launch! 🚀 #launch",
    "platforms": ["twitter-123456789"],
    "scheduledTime": "2026-03-01T14:00:00.000Z"
  }'
Situation Result
Key not seen before Request runs normally; the response is recorded against the key
Same key + same body The original response is replayed verbatim (same status, same postGroupId). No second post.
Same key + different body 422 / IDEMPOTENCY_KEY_CONFLICT
Same key, first request still running 409 / IDEMPOTENCY_IN_FLIGHT — retry shortly

Opt-in. Without the header, behaviour is exactly as before — no dedup, no new failure modes.

Client guidance:

  • Generate a fresh key per logical create (e.g. crypto.randomUUID()), and reuse that same key for every retry of that one create — that is the entire point.
  • Never reuse a key across different posts; the second one gets 422.
  • On 409, wait and retry the identical request rather than dropping the key — a stuck in-flight claim frees up after ~3 minutes and the retry then completes or replays.
  • Keys are scoped to the acting user (the managed user when x-publora-user-id is set), so keys cannot collide across accounts.
  • Records expire after 24 hours. After that a reused key is treated as new and creates a second post — keep retries well inside that window.
  • Body comparison is order-insensitive (JSON keys are canonicalized), so re-serializing the same payload does not trip a conflict. Extremely deeply nested bodies (>200 levels) are rejected with 400 / IDEMPOTENCY_BODY_TOO_COMPLEX.

Posts with Media

Publishable media-required platforms (Instagram, TikTok, YouTube): these platforms require media on every post. create-post validates this at the scheduling gate: if scheduledTime is set (so the post is created as scheduled) and one of these platforms has no media, the call is rejected up front with HTTP 400 MEDIA_REQUIRED — the validator runs with an empty media list. It is not accepted-then-failed-at-publish. Two ways to satisfy it: (1) pass mediaUrls (public https URLs) in the same create-post call to attach and schedule in one shot, or (2) create a draft (omit scheduledTime), attach media, then schedule via Update Post. Creating a draft with no media is always allowed (drafts skip validation). Pinterest is connect-only: its registry limits and media validation do not imply publishing support. See Validation for the per-platform media matrix.

Two supported media flows:

A. One-shot with mediaUrls (public https URLs — best for agents):

POST /create-post   → content + platforms + scheduledTime + mediaUrls[]  (attached + scheduled in one call)

B. Draft → upload → schedule (for local files):

1. POST /create-post              → Omit scheduledTime (creates draft)
2. POST /get-upload-url           → Get pre-signed URL for media
3. PUT {uploadUrl}                → Upload file to S3
4. POST /complete-media/:mediaId  → (optional) finalize/validate the upload early
5. PUT /update-post/:postGroupId  → Set status="scheduled" + scheduledTime

Do not attach media via get-upload-url to an already-scheduled post: attaching media demotes the post back to draft (postGroupDemoted: true) and you must re-schedule. See Upload Media.

This ensures media is fully uploaded before the scheduler processes your post. See Upload Media for details.

Default Platform Settings

When creating via the API, these defaults are applied automatically. If you provide a platformSettings object in the request body, your values are merged with these defaults on a per-platform basis — user-provided fields override the corresponding default fields, while any default fields you omit are preserved. See Default Platform Settings for the full list.

{
  "tiktok": {
    "viewerSetting": "PUBLIC_TO_EVERYONE",
    "allowComments": true,
    "allowDuet": false,
    "allowStitch": false,
    "commercialContent": false,
    "brandOrganic": false,
    "brandedContent": false
  },
  "instagram": {
    "videoType": "REELS"
  },
  "youtube": {
    "privacy": "public",
    "title": "",
    "madeForKids": false,
    "playlist": {
      "id": "",
      "platformId": ""
    }
  },
  "threads": {
    "replyControl": ""
  },
  "telegram": {
    "disableNotification": false,
    "disableWebPagePreview": false,
    "protectContent": false
  }
}

Note: Only tiktok, instagram, youtube, threads, telegram, and linkedin keys are recognized in platformSettings. Other platform keys (e.g., twitter, facebook, bluesky, mastodon) are rejected with 400 / PLATFORM_SETTING_UNKNOWN — they are no longer silently dropped. LinkedIn accepts repostEnabled, repostParentUrn, and repostVisibility. For telegram, only the three boolean keys shown above (disableNotification, disableWebPagePreview, protectContent) are accepted; any other key inside the telegram object is likewise a 400, and string values like "false" / "0" / "off" are still coerced to false. See Unknown platformSettings paths.

YouTube also accepts tags, categoryId, madeForKids, and a playlist object ({ id, platformId }) in addition to privacy/title. A playlist can be set directly on create-post. Nested objects are validated to their leaves: playlist accepts only id/platformId and thumbnail only mediaId/id/url/path — any other nested key (e.g. youtube.thumbnail.mediaID) returns 400 / PLATFORM_SETTING_UNKNOWN rather than silently clearing the value. A custom thumbnail cannot be set on create-post — the thumbnail upload requires a postGroupId, so create the post first and set the thumbnail via update-post. See YouTube → Platform-Specific Settings for the full field reference.

Instagram also accepts coverUrl (alias: cover_url) — a custom cover image for Reels. Provide a publicly accessible http(s) URL to a JPEG image (Instagram fetches it server-side at publish time), or upload a cover file (JPEG/PNG/WebP up to 8 MB) and use the URL it returns. Non-JPEG or non-http(s) URLs are rejected with 400. An empty string clears the custom cover. Ignored for Stories and image posts. See Instagram → Platform-Specific Settings.

LinkedIn Repost Settings

Set platformSettings.linkedin.repostParentUrn to schedule a repost (reshare) of an existing LinkedIn post instead of a regular post. The group's content becomes the reshare commentary shown above the reposted post (the normal LinkedIn 3,000-character content limit applies); empty content is allowed for a plain repost.

{
  "content": "My commentary on this great post.",
  "platforms": ["linkedin-Tz9W5i6ZYG"],
  "scheduledTime": "2026-08-01T14:00:00.000Z",
  "platformSettings": {
    "linkedin": {
      "repostParentUrn": "urn:li:share:7123456789012345678",
      "repostVisibility": "PUBLIC"
    }
  }
}
Field Type Description
repostParentUrn string URN of the post to reshare — urn:li:share:<id> or urn:li:ugcPost:<id> (not urn:li:activity:<id>). Empty string = normal post (repost off)
repostVisibility string PUBLIC (default) or CONNECTIONS. Lowercase input is normalized to uppercase. CONNECTIONS is only valid for personal accounts — company (organization) pages are rejected at intake (see the accepted tree) and are always forced to PUBLIC at publish time
repostEnabled boolean Optional explicit toggle. true requires a non-empty repostParentUrn; false requires an empty one. When omitted, a non-empty repostParentUrn enables the repost

Rules:

  • Media is forbidden on repost groups. Scheduling a repost group with attached media fails validation with code MEDIA_TYPE_NOT_SUPPORTED: "LinkedIn reposts cannot include media — remove the attached files or turn off the repost setting".
  • Publish-time failures: if the parent post was deleted, made private, or is not reshareable, the scheduled post fails permanently with "LinkedIn rejected the repost — the original post may have been deleted, made private, or is not reshareable."

Field-level validation errors (400):

Error Cause
"platformSettings.linkedin.repostParentUrn must be a LinkedIn post URN like urn:li:share:<id> or urn:li:ugcPost:<id>" Invalid URN shape (activity URNs are rejected)
"platformSettings.linkedin.repostVisibility must be one of: PUBLIC, CONNECTIONS" Invalid visibility value
"platformSettings.linkedin.repostParentUrn is required when repostEnabled is true" repostEnabled: true without a parent URN
"platformSettings.linkedin.repostParentUrn must be empty when repostEnabled is false" repostEnabled: false with a parent URN set

Tip: To repost immediately without scheduling, use the dedicated LinkedIn Reshare endpoint (POST /linkedin-reshare) instead.

Examples

Schedule a text post to X and LinkedIn

JavaScript (fetch)

const response = await fetch('https://api.publora.com/api/v1/create-post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-publora-key': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    content: 'Excited to share our new product launch! 🚀 #launch',
    platforms: ['twitter-123456789', 'linkedin-ABC123'],
    scheduledTime: '2026-03-01T14:00:00.000Z'
  })
});
const data = await response.json();
console.log(data.postGroupId);

Python (requests)

import requests
 
response = requests.post(
    'https://api.publora.com/api/v1/create-post',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY'
    },
    json={
        'content': 'Excited to share our new product launch! 🚀 #launch',
        'platforms': ['twitter-123456789', 'linkedin-ABC123'],
        'scheduledTime': '2026-03-01T14:00:00.000Z'
    }
)
print(response.json()['postGroupId'])

cURL

curl -X POST https://api.publora.com/api/v1/create-post \
  -H "Content-Type: application/json" \
  -H "x-publora-key: YOUR_API_KEY" \
  -d '{
    "content": "Excited to share our new product launch! 🚀 #launch",
    "platforms": ["twitter-123456789", "linkedin-ABC123"],
    "scheduledTime": "2026-03-01T14:00:00.000Z"
  }'

Node.js (axios)

const axios = require('axios');
 
const { data } = await axios.post(
  'https://api.publora.com/api/v1/create-post',
  {
    content: 'Excited to share our new product launch! 🚀 #launch',
    platforms: ['twitter-123456789', 'linkedin-ABC123'],
    scheduledTime: '2026-03-01T14:00:00.000Z'
  },
  { headers: { 'x-publora-key': 'YOUR_API_KEY' } }
);
console.log(data.postGroupId);

Post to all 10 platforms at once

Note: This example shows the platform-ID format and fan-out only. As written it is text-only with scheduledTime set, so it is rejected with HTTP 400 MEDIA_REQUIRED — the Instagram, TikTok, and YouTube targets require media. To include them, either pass mediaUrls in this same call, or create as a draft (omit scheduledTime), upload media, then schedule. Pinterest is connect-only and is intentionally absent: satisfying its registry media validation does not make it publishable.

const response = await fetch('https://api.publora.com/api/v1/create-post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-publora-key': 'YOUR_API_KEY'
  },
  body: JSON.stringify({
    content: 'Big announcement dropping tomorrow. Stay tuned.',
    platforms: [
      'twitter-111', 'linkedin-222', 'instagram-333',
      'threads-444', 'tiktok-555', 'youtube-666',
      'facebook-777', 'bluesky-888', 'mastodon-999',
      'telegram-000'
    ],
    scheduledTime: '2026-03-01T09:00:00.000Z'
  })
});

Create a draft (no scheduled time)

response = requests.post(
    'https://api.publora.com/api/v1/create-post',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY'
    },
    json={
        'content': 'Draft post -- will schedule later',
        'platforms': ['twitter-123456789']
    }
)

Errors

Platform IDs use <lowercase-prefix>-<id>. The prefix must contain only lowercase ASCII letters. The ID must be non-empty and cannot contain whitespace, /, ?, or #; other characters, including colons, are allowed. For example, bluesky-did:plc:abc123 is a valid target.

Status Error Cause
400 "Content is required" Missing content field or content is empty/whitespace-only
400 "Platforms are required" platforms field is missing or null
400 "At least one platform is required" platforms is an empty array
400 "Platforms must be an array" platforms is not an array
400 "Invalid platforms JSON format" platforms contains malformed JSON
400 "Invalid platform ID format: <id>" Prefix is not lowercase letters, or the ID is empty or contains whitespace, /, ?, or #
400 "Invalid scheduled time format" scheduledTime is not a valid ISO 8601 datetime
400 "Scheduled time is in the past. Server time is <ISO> UTC." code: "SCHEDULED_TIME_IN_PAST", serverTime: "<ISO>". scheduledTime is 5+ minutes in the past while strict mode is active; the default transition is scheduled for 2026-08-25 but configuration can override it.
400 "Unknown platformSettings path: <path>" code: "PLATFORM_SETTING_UNKNOWN", field: "<exact.path>". Unknown platform or nested key; nothing is persisted. See Unknown platformSettings paths
400 "platformSettings.linkedin.*" validation errors Invalid LinkedIn repost settings (bad URN shape, bad visibility, repostEnabled/repostParentUrn mismatch) — see LinkedIn Repost Settings
400 "LinkedIn company-page reposts cannot use CONNECTIONS visibility; choose PUBLIC" repostVisibility: "CONNECTIONS" while the scheduled group targets a LinkedIn company-page connection
400 "Invalid platformSettings JSON" platformSettings was provided as a string that could not be parsed as valid JSON
400 "Idempotency-Key request body is too deeply nested" code: "IDEMPOTENCY_BODY_TOO_COMPLEX". Body nested more than 200 levels deep while using Idempotency-Key
400 "mediaUrls must be an array of public https URLs" mediaUrls is present but not an array
400 "mediaUrls supports at most 10 URLs per request" More than 10 URLs supplied (also: "mediaUrls must contain at least one URL when provided", "Every mediaUrls entry must be a non-empty string")
400 "Media ingestion failed" One or more mediaUrls could not be downloaded/probed; per-URL detail in the mediaResults array
429 "Media URL ingestion rate limit reached (60 URLs/hour)…" code: "MEDIA_URL_RATE_LIMITED", retryAfterSec + Retry-After header
400 "platformSettings must be an object" platformSettings is provided but is not a plain object (e.g., array, null, or primitive after JSON parsing)
400 "Invalid x-publora-user-id" The x-publora-user-id header value is not a valid ObjectId format
401 "API key is required" Missing x-publora-key header
401 "Invalid API key" x-publora-key value is incorrect or revoked
401 "Invalid API key owner" API key exists but the associated workspace/user could not be resolved
403 "API access is not enabled for this account" The account has apiAccess disabled (a custom plan; standard plans including the free Starter plan include API access)
403 "MCP access is not enabled for this account" Returned when x-publora-client: mcp is set but MCP access is not enabled
403 "Workspace access is not enabled for this key" The API key does not have workspace/managed-user permissions
403 "User is not managed by key" The x-publora-user-id references a user not managed by this API key
403 LimitExceededError (structured) Plan limit reached (see below)
409 "A request with this idempotency key is still in flight" code: "IDEMPOTENCY_IN_FLIGHT". An earlier request with the same Idempotency-Key has not finished. Retry the identical request shortly. See Idempotency
422 "Idempotency key was already used with a different request body" code: "IDEMPOTENCY_KEY_CONFLICT". The same Idempotency-Key was reused with a different body. Generate a fresh key per logical create
500 "Failed to create post group" Unexpected server error

Publishable media-required platforms (Instagram, TikTok, YouTube): when scheduledTime is set, create-post validates media presence and rejects a media-less post with HTTP 400 MEDIA_REQUIRED (the validator runs with an empty media list). Attach media first — pass mediaUrls, or use the draft flow. Pinterest is connect-only and cannot be published even if media validation passes. See Posts with Media and Validation. The 400 body shape:

{
  "error": "Validation failed",
  "validation": {
    "valid": false,
    "errors": [
      {
        "code": "MEDIA_REQUIRED",
        "message": "Instagram posts require media",
        "platform": "instagram",
        "field": "media",
        "suggestions": [
          "Upload an image or video for this platform",
          "This platform requires media. One-shot fix: POST /api/v1/create-post with mediaUrls (array of public https URLs) plus scheduledTime. Manual flow: create a draft (no scheduledTime), POST /api/v1/get-upload-url once per file, PUT the bytes with the same Content-Type, POST /api/v1/complete-media/:mediaId, then PUT /api/v1/update-post/:postGroupId with status='scheduled'."
        ],
        "severity": "error"
      }
    ],
    "warnings": [],
    "summary": { "errorCount": 1, "warningCount": 0, "affectedPlatforms": ["instagram"] }
  }
}

(The second suggestions entry is the channel-aware recovery hint — MCP callers get the create_post/get_upload_url tool-name variant.)

LimitExceededError (403)

When a plan limit is exceeded, the API returns a structured error response. The error field contains a short label, while the message field contains the full human-readable explanation.

Note: The first time a post limit is exceeded in a given billing month, an email notification is sent to the account owner. This notification is sent only once per month.

{
  "error": "Post limit reached",
  "code": "POST_LIMIT_REACHED",
  "metric": "posts.platform_monthly",
  "message": "Monthly post limit reached. Your Pro plan allows 100 platform posts per month.",
  "limit": 100,
  "used": 100,
  "requested": 2,
  "remaining": 0,
  "periodStart": "2026-03-01T00:00:00.000Z",
  "periodEnd": "2026-04-01T00:00:00.000Z",
  "planName": "Pro"
}

Note: Not all fields are present on every error code. For example, SCHEDULE_HORIZON_REACHED returns null for used, requested, remaining, periodStart, and periodEnd.

Some limit errors include additional context fields:

Field Present on Description
scheduledTime SCHEDULE_HORIZON_REACHED The requested scheduled time that exceeded the horizon
maxScheduledDate SCHEDULE_HORIZON_REACHED The furthest date allowed by the current plan
limitScope POST_LIMIT_REACHED "account" or "connection", matching the plan's monthly-post scope
blockedPlatforms POST_LIMIT_REACHED Per-connection { platformSelection, used, remaining } entries; present only for a connection-scoped monthly-post rejection
overLimitBy CONNECTIONS_OVER_LIMIT Number of connections over the plan limit
disallowedPlatforms PLATFORM_NOT_AVAILABLE Array of platform names not available on the current plan
allowedPlatforms PLATFORM_NOT_AVAILABLE Array of platform names available on the current plan

Possible code values:

Code Error value Description
POST_LIMIT_REACHED "Post limit reached" Monthly post limit exceeded
SCHEDULED_POST_LIMIT_REACHED "Scheduled post limit reached" Scheduled post limit exceeded
SCHEDULE_HORIZON_REACHED "Schedule horizon reached" Scheduling too far in the future for current plan
CONNECTIONS_OVER_LIMIT "Account over channel limit" Too many platform connections for current plan
PLATFORM_NOT_AVAILABLE "Platform not available" Platform not available on current plan (e.g., a custom plan with restricted allowedPlatforms). Metric: posts.platform_monthly.

Post Statuses

After creation, the post group goes through these states:

draft → scheduled → published
                  → failed
                  → partially_published

Post group statuses (returned in status field):

  • draft: Saved but not scheduled
  • scheduled: Will be published at scheduledTime
  • published: Successfully posted on all platforms
  • failed: Failed on all platforms
  • partially_published: Succeeded on some, failed on others

Note: Individual platform records (ScheduledPost) have their own statuses including pending and processing, which reflect per-platform delivery state. The post group status field above is a rollup and does not include processing. A separate processingStatus field on the post group tracks whether the post is currently being processed. Possible processingStatus values are: pending, processing, finished.

Note: processingStatus is internal and is not returned by get-post or list-posts.


Publora is built by Creative Content Crafts, Inc. Need AI-powered content creation for LinkedIn, Threads, and X? Try Co.Actor — the best AI service for authentic thought leadership at scale.