Chirio
Get Started
Introduction
Quickstart
How Chirio works
Authentication
Platforms
Overview
Instagram
Threads
X
LinkedIn
Guides
Connect accounts
Publish a post
Media requirements
Carousels
First comments
Idempotency and retries
Account lifecycle
Revocation and deletion
The dashboard
Billing
Plans
Metering
Quotas
Api Reference
Overview
Accounts
Posts
Errors
Changelog
Roadmap
TrademarkTrademark
Ctrl k
Search...
Sign up
Chirio
Get Started
Introduction
Quickstart
How Chirio works
Authentication
Platforms
Overview
Instagram
Threads
X
LinkedIn
Guides
Connect accounts
Publish a post
Media requirements
Carousels
First comments
Idempotency and retries
Account lifecycle
Revocation and deletion
The dashboard
Billing
Plans
Metering
Quotas
Api Reference
Overview
Accounts
Posts
Errors
Changelog
Roadmap
TrademarkTrademark© Dopler. All rights reserved.
Built with Aveiro
Guides

Publish a post

One call, many targets — and how to read a result where the targets disagree.
Updated 2d ago
Connect accounts
Media requirements
POST /api/v1/posts takes one piece of content and up to ten targets, publishes to all of them concurrently, and returns what happened on each.
curl -X POST "https://chirio.dev/api/v1/posts" \
  -H "Authorization: Bearer $CHIRIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: release-2-4-0" \
  -d '{
    "content": "Chirio 2.4 is out.",
    "mediaItems": [{ "type": "image", "url": "https://cdn.yourapp.com/2-4.jpg" }],
    "platforms": [
      { "platform": "instagram", "accountId": "8f2c..." },
      { "platform": "threads",   "accountId": "d31a..." },
      { "platform": "linkedin",  "accountId": "77bb...", "firstComment": "Release notes: https://example.com/2-4" }
    ]
  }'

Request body

Field
Type
Rules
`content`stringUp to 10,000 chars at the API; each platform enforces its own shorter limit. Defaults to `""`.
`mediaItems`array0–10 items of `{ type: "image" \
`platforms`array1–10 targets of `{ platform, accountId, firstComment? }`.
`idempotencyKey`string1–255 chars. Also accepted as the `Idempotency-Key` header; the body field wins.
firstComment is per target, not per post, so the same publish can carry different comment wording for LinkedIn and Instagram. See First comments.

What is validated up front

Before any network call, Chirio checks in this order:
  • Body shape — schema violations answer 400 invalid_request.
  • Accounts exist and belong to your project — otherwise 404 account_not_found.
  • Platform matches the account — targeting a threads account as instagram answers 400 platform_mismatch.
  • Per-platform content rules — length, media counts, video/image mixing. A violation on any target rejects the whole request with 400 invalid_post, before anything publishes.
  • Quota — 429 quota_exceeded on a plan with no overage. See Quotas.
That ordering is deliberate: everything you can fix is checked before a single byte reaches a platform, so a rejected request never leaves a half-published post behind.

Reading the response

201 Created (or 200 with replayed: true for an idempotent retry):
{
  "post": {
    "id": "3a1e...",
    "content": "Chirio 2.4 is out.",
    "media": [{ "type": "image", "url": "https://cdn.yourapp.com/2-4.jpg" }],
    "createdAt": "2026-08-04T09:12:44.101Z",
    "targets": [
      {
        "id": "b77d...",
        "accountId": "8f2c...",
        "platform": "instagram",
        "status": "published",
        "platformPostId": "17851...",
        "url": "https://www.instagram.com/p/...",
        "error": null,
        "publishedAt": "2026-08-04T09:12:51.882Z",
        "commentStatus": "none",
        "commentId": null,
        "commentError": null
      },
      {
        "id": "c02f...",
        "accountId": "d31a...",
        "platform": "threads",
        "status": "failed",
        "platformPostId": null,
        "url": null,
        "error": "media_processing_timeout: container did not finish processing",
        "publishedAt": null,
        "commentStatus": "none",
        "commentId": null,
        "commentError": null
      }
    ]
  },
  "replayed": false
}

Target fields

Field
Meaning
`status``published` or `failed`
`platformPostId`The platform's own id
`url`Public URL of the post, when the platform returns one
`error``code: message` on failure, `null` otherwise
`publishedAt`When the platform accepted it
`commentStatus``none`, `published`, `failed` or `unsupported` — independent of `status`
`commentId` / `commentError`Outcome of the first comment

Mixed results are normal

Do not treat 201 as success
A 201 means the request was accepted and every target was attempted. Targets fail independently — one platform's rate limit, expired connection or media error does not stop the others. Always iterate post.targets and act per target.
Retrying only the failed targets is also the cheap thing to do: usage is recorded per target that actually published, so a retry of the third target bills the third target only.

Timing

The call is synchronous and waits for every platform. Image posts settle in seconds. Video is different — the platform processes the file server-side and Chirio waits for it, up to roughly four minutes per item, so a video-heavy carousel can take several minutes.
Two consequences worth designing for:
  • Send an Idempotency-Key so a client-side timeout is safe to retry. See Idempotency.
  • Poll GET /api/v1/posts/:id when a replayed response shows targets that have not settled yet.

Listing history

Newest first. limit is capped at 100 and defaults to 25.
const { post } = await res.json();

const failed = post.targets.filter((t) => t.status === "failed");
if (failed.length > 0) {
  // Retry only these accountIds, with a NEW idempotency key.
  console.warn(failed.map((t) => `${t.platform}: ${t.error}`));
}
curl "https://chirio.dev/api/v1/posts?limit=25&offset=0" \
  -H "Authorization: Bearer $CHIRIO_API_KEY"