Let your Claude, Codex, OpenClaw, Hermes & more manage your social mediaLearn more

API Reference

The Postally REST API lets you upload media and schedule or publish posts to every connected platform — X, Instagram, LinkedIn, Facebook, Threads, TikTok, YouTube, and Bluesky — from your own code. All endpoints are authenticated with an API key and return JSON.

Base URL

https://app.postally.io/api/public/v1

Every path below is relative to this base URL, e.g. POST https://app.postally.io/api/public/v1/posts.

Authentication

Every request must include two headers: your API key and the brand you're acting on.

Authorization: Bearer pst_live_xxxxxxxxxxxxxxxxxxxx
x-brand-id: <your_brand_id>
Content-Type: application/json

Getting an API key

Generate a key in your dashboard under Settings → API Keys. Choose the scopes it needs, then copy the token — it starts with pst_live_ (or pst_test_) and is shown only once.

The x-brand-id header

A Postally account can hold several brands. The x-brand-id header selects which brand every request acts on. You can find your Brand ID in the dashboard, or by calling GET /me.

Keep your key secret

Treat API keys like passwords. Use them only from your server — never expose them in browser or mobile client code. Revoke a key from Settings → API Keys if it's ever leaked.

Scopes

Each API key is granted one or more scopes. A request to an endpoint without its required scope returns 403 INSUFFICIENT_SCOPE.

ScopeGrants
posts:readList and read posts
posts:writeCreate, schedule, reschedule, and delete posts
media:writeUpload media (files and remote URLs)
analytics:readRead profile and post analytics
brands:readList brands and read account context

Account context

GET/mebrands:read

Returns the organization, brand, and user resolved from your API key and x-brand-id header. Handy for confirming a key and brand are wired up correctly.

Response

{
  "organization": { "id": "org_...", "name": "Acme Inc" },
  "brand": { "id": "brand_...", "name": "Acme" },
  "user": { "id": "usr_...", "email": "you@acme.com", "name": "You" }
}
GET/brandsbrands:read

List every brand in the API key's organization. Use GET /brands/:id to fetch a single brand.

Upload media

Upload an image or video first, then attach the returned id and path to a post. Both endpoints return the same media object.

POST/media/uploadmedia:write

Upload a file as multipart/form-data in a field named file. Supports common image and video formats.

Response

{
  "id": "media_...",
  "name": "launch.png",
  "path": "https://cdn.postally.io/media/launch.png",
  "thumbnail": "https://cdn.postally.io/media/launch_thumb.png",
  "alt": null
}
POST/media/upload-from-urlmedia:write

Hand Postally a public URL and it downloads and stores the file for you. Returns the same media object as above.

Request body

{
  "url": "https://example.com/image.jpg"
}

Publish & schedule posts

POST/postsposts:write

Create a draft, schedule a post for later, or publish immediately. A single call can target multiple platforms — add one entry to posts[] per connected account.

Top-level fields

  • type — one of draft, schedule, or now. Draft saves without publishing; schedule publishes at date; now publishes immediately.
  • date — ISO 8601 datetime (required). Must be in the future for schedule. Ignored for now.
  • shortLink — boolean (required). Auto-shorten URLs in the content.
  • posts[] — one entry per target account (required unless type is draft).

Each posts[] entry

  • integration.id — the connected account to post to. This is what selects the platform (see “Finding integration IDs” below).
  • value[] — the content blocks. One block is a normal post; multiple blocks form a thread / multi-part post. Each block has content (text) and image[] (media from the upload step).
  • settings.__type — the platform identifier; must match the integration's platform. Some platforms require extra fields here (see the settings reference below).
  • commentDelays[] — optional per-block delay in minutes (0–1440) for pacing threads.

Request body

{
  "type": "schedule",
  "date": "2026-09-01T14:30:00Z",
  "shortLink": false,
  "posts": [
    {
      "integration": { "id": "integration_x_123" },
      "value": [
        {
          "content": "Launching something new today \ud83d\ude80",
          "image": [
            {
              "id": "media_...",
              "path": "https://cdn.postally.io/media/launch.png"
            }
          ]
        }
      ],
      "settings": { "__type": "x" }
    }
  ]
}

Response

[
  {
    "postId": "post_...",
    "integration": "integration_x_123",
    "group": "group_..."
  }
]

Platform settings (settings.__type)

Set __type to the target platform. Most platforms need only { "__type": "..." }; a few require extra fields, listed here.

__typePlatformRequired extra fields
xX (Twitter)
instagramInstagrampost_type: "post" | "story"
linkedinLinkedIn
linkedin-pageLinkedIn Page
facebookFacebook
threadsThreads
tiktokTikTokprivacy_level, duet, stitch, comment, autoAddMusic, brand_content_toggle, brand_organic_toggle, content_posting_method
youtubeYouTubetitle (2–100), type: "public" | "private" | "unlisted"
blueskyBluesky

Finding integration IDs

There is no public “list connected accounts” endpoint yet. Get an integration.id from your dashboard, or read it from the integration.id field returned by GET /posts. Posting with an unknown ID returns a 400.

Manage posts

GET/posts?startDate&endDateposts:read

List posts (scheduled and published) between two ISO 8601 dates. Each item includes its integration, state, and publishDate.

GET/posts/:idposts:read

Fetch a single post, including its content blocks, attached media, platform settings, and group.

PUT/posts/:id/dateposts:write

Reschedule a post to a new time.

Request body

{ "date": "2026-09-05T09:00:00Z" }
DELETE/posts/:groupposts:write

Delete a post group (identified by the group returned when the post was created). Removes all platform copies in that group.

Analytics

GET/analytics/insights/profile?integrationId=analytics:read

Return profile-level metrics plus a list of recent posts with per-post metrics for one connected account. Pass the integrationId as a query parameter.

Errors

Errors return a JSON body with a machine-readable code and a human-readable message:

{
  "statusCode": 403,
  "code": "INSUFFICIENT_SCOPE",
  "message": "This API key is missing the required scope."
}
StatuscodeMeaning
401API_KEY_REQUIREDMissing or malformed Authorization header
401INVALID_API_KEYKey is invalid or revoked
400BRAND_ID_REQUIREDMissing x-brand-id header
403BRAND_NOT_ALLOWEDKey is not permitted to act on this brand
403INSUFFICIENT_SCOPEKey is missing the required scope
402Storage limit exceeded (media upload)
400Platform validation failedPost content/settings invalid — see issues[]
429Rate limit exceeded

Rate limits

Requests are throttled per API key at roughly 60 requests per minute. Exceeding the limit returns 429 Too Many Requests — back off and retry.

Interactive reference

A live OpenAPI (Swagger) reference for these endpoints is available at https://app.postally.io/api/public/v1/docs.