Home / Docs / API guide
Public API

Public API guide

Everything you need before you read the reference: how to sign requests, schedule your first post, read errors and stay inside the limits.

Developer docs: Overview, API guide, API reference, MCP server, Webhooks, CLI, OAuth for apps. Machine readable: openapi.json.

Base URL

All paths in the API reference start with:

https://app.postozo.com/public/v1

Requests and answers are JSON (UTF-8), except file uploads, which are multipart form data. A JSON body can be up to 5 MB.

Authentication

Send your key in the Authorization header on every request. The Bearer prefix is optional.

Authorization: pz_live_YOUR_KEY
Authorization: Bearer pz_live_YOUR_KEY
TokenStarts withWhere it comes fromActs as
Workspace API keypz_live_Settings, Developers (owners and admins only). 1 key per workspace.An admin of that workspace. Posts show "API" as the author.
OAuth tokenpz_oat_An app the person approved with OAuth.The person who approved, with their role in that workspace.
CLI tokenpz_cli_postozo auth:login (CLI).The person who approved, with their role.

Keep keys out of code you share and out of logs. If a key leaks, press Rotate Key in Settings, Developers: the old key stops working at once. Tokens for apps and the CLI are listed in Settings, Approved Apps, where you can revoke each 1. A workspace key also stops working when the workspace owner's account is suspended.

Your first post

1. Find the id of a channel:

curl "https://app.postozo.com/public/v1/integrations" \
  -H "Authorization: pz_live_YOUR_KEY"
[
  {
    "id": "de53f029-255d-4104-b090-974efcca2c86",
    "name": "acme",
    "identifier": "demo",
    "picture": null,
    "disabled": false,
    "refreshNeeded": false,
    "inBetweenSteps": false,
    "profile": "acme"
  }
]

2. Check the network's rules for that channel (limit, media, required settings):

curl "https://app.postozo.com/public/v1/integration-settings/de53f029-255d-4104-b090-974efcca2c86" \
  -H "Authorization: pz_live_YOUR_KEY"

3. Schedule a post with a first comment 5 minutes later:

curl -X POST "https://app.postozo.com/public/v1/posts" \
  -H "Authorization: pz_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "schedule",
  "date": "2026-10-01T09:30:00+05:30",
  "posts": [
    {
      "integration": {
        "id": "de53f029-255d-4104-b090-974efcca2c86"
      },
      "value": [
        {
          "content": "Our autumn menu is here."
        },
        {
          "content": "Book a table: https://example.com",
          "delay": 5
        }
      ],
      "settings": {}
    }
  ]
}'

The answer has 1 row per channel with the postId, the group id of the whole post and a previewUrl you can share. Use "type": "now" to publish right away, "draft" to save it only, or "review" to send it to your client first. The full body is in the POST /posts reference.

4. Check the result later with GET /posts/{id}. Each channel has a state: queued, publishing, published with a releaseUrl, or error with a message and a fix. Or add a webhook and get told.

Errors

Every error is JSON with the message twice: error and msg (the Postiz name). Some errors add more fields:

  • errors: a list of problems per channel when a post breaks a network rule, each { channelId, channelName, message }.
  • fix: what to do. Plan limits, a workspace with no plan or a failed payment, paused access, ids that are not valid, unknown paths, conflicts and server errors all have 1.
  • code: a stable name for some errors, for example no_subscription, payment_failed, idempotency_key_reused, email_not_verified or member_paused.
{
  "error": "Please fix these before saving.",
  "msg": "Please fix these before saving.",
  "errors": [
    {
      "channelId": "de53f029-255d-4104-b090-974efcca2c86",
      "channelName": "acme (Demo)",
      "message": "acme (Demo): the post is too long, 1200 of 1000 characters."
    }
  ]
}
StatusWhen
400The request cannot be done as sent: a missing or wrong field, a post that breaks a network rule (see errors), a date in the past, or JSON that does not parse.
401"No API Key found" (no header), "Invalid API key" (wrong, rotated or revoked key) or "No subscription found" (the trial ended without a plan, or a cancelled plan ended; every call is refused until a plan is picked).
402A plan limit, for example more channels than the plan allows, or "Publishing is paused because the last payment failed." (the 7 day grace is over; every call is refused until it is paid). The answer has a fix.
403Your role cannot do this (for example an editor's token deleting a channel), the owner has not confirmed their email yet, or your access is paused because the plan has no team.
404The post, channel, media or network does not exist in this workspace.
413The JSON body is over 5 MB, or an upload is over the largest file size the server takes. An image over the image limit gets 400 with the limit in the message.
409The same Idempotency-Key with a different body (or while the first request is still running), or something with the same unique value already exists.
429Too many requests. The Retry-After header gives the seconds to wait. See the limits below.
500Something went wrong on our side. The answer has error, msg and fix. Try again; if it keeps happening, contact support.

Errors of a post that failed on the network are not HTTP errors: the post is saved and its channel gets state: "error" with error: { message, fix }, for example "The post is longer than this network allows." with "Shorten the text for this channel, then retry."

Every common error, exactly as the server sends it:

StatusWhenBody
401No Authorization header.{"error":"No API Key found","msg":"No API Key found"}
401The key or token is wrong, was rotated or was revoked.{"error":"Invalid API key","msg":"Invalid API key"}
400The body is not valid JSON.{"error":"The request body is not valid JSON.","msg":"The request body is not valid JSON."}
404The post, channel or media does not exist in this workspace.{"error":"Post not found","msg":"Post not found"}
400An id in the path is not in the id format (a UUID).{"error":"One of the ids is not valid.","msg":"One of the ids is not valid.","fix":"Check the id. Copy it again from the list it came from."}
404The path does not exist.{"error":"Not found","msg":"Not found","fix":"Check the method and the path. The API reference lists every path."}
401The workspace has no plan: the trial ended without a plan, or a cancelled plan ended. Every call is refused until a plan is picked. MCP tool calls get the same body.{"error":"No subscription found","msg":"No subscription found","code":"no_subscription","fix":"The workspace owner should choose a plan on the Billing page of the app. Nothing was deleted."}
402Publishing is paused: the 7 day grace after a failed payment is over. Every call is refused until the payment goes through. MCP tool calls get the same body.{"error":"Publishing is paused because the last payment failed.","msg":"Publishing is paused because the last payment failed.","code":"payment_failed","fix":"The workspace owner should pay the open invoice on the Billing page of the app. Nothing was deleted, and posts go out again once it is paid."}
409Something with the same unique value already exists.{"error":"That already exists.","msg":"That already exists.","fix":"Use a different name or value, or change the one that already exists."}
429Over a rate limit. The Retry-After header gives the seconds to wait.{"error":"Too many requests. Try again in 42 minutes.","msg":"Too many requests. Try again in 42 minutes."}
500Something went wrong on our side.{"error":"Something went wrong on our side. Try again. If it keeps happening, contact support.","msg":"Something went wrong on our side. Try again. If it keeps happening, contact support.","fix":"Try again in a minute. If it keeps happening, email support@postozo.com with the time of the request."}

Rate limits

WhatLimitCounted per
POST /public/v1/posts90 requests an hourWorkspace
MCP (/mcp)600 requests a minuteWorkspace
POST /oauth/register30 an hourIP address
POST /oauth/token300 an hourIP address
POST /oauth/device/code30 an hourIP address
All other API endpointsNo fixed limit

Each window starts with the first request and lasts the full hour (or minute). Over the limit, the answer is 429 with {"error": "Too many requests. Try again in 42 minutes.", "msg": "..."} and a Retry-After header with the seconds until the window starts again, for example Retry-After: 2520. The limits are counted by the server in memory, so they start again after a server restart. 1 request to POST /posts can hold many channels, so put all the channels of 1 post in 1 request.

Paging

  • GET /posts has no pages. It returns every post between startDate and endDate (default: 30 days back to 30 days ahead). Ask for shorter ranges to get fewer rows.
  • GET /media returns 40 files a page. Send page=2, page=3 and so on. A page with fewer than 40 files is the last.
  • GET /notifications returns 50 a page with the same page parameter, and echoes the page number.
  • GET /integrations, GET /groups and GET /networks return everything at once.

Dates and time zones

  • Send dates as ISO 8601 with a time zone: 2026-10-01T09:30:00+05:30 or 2026-10-01T04:00:00Z. Without a zone, the server's own zone is used, so always add 1.
  • Answers are always UTC with milliseconds, for example 2026-10-01T04:00:00.000Z.
  • A schedule or review post cannot be more than 1 minute in the past. Drafts can have any date. now ignores the date.
  • The free slot endpoints (GET /find-slot and the MCP tool find_free_slot) read each channel's posting times (set in the app) as local times of day. Send tzOffsetMinutes, the minutes to add to UTC to get your local time (330 for India, -300 for New York in winter). Without it, the time zone in the profile of the person the key belongs to is used (the owner's for the workspace key), with daylight saving time; UTC when none is set. The answer is UTC: 9:00 in India comes back as 03:30:00.000Z.
  • A date that does not parse gives 400 "The date is not valid. Use ISO 8601, for example 2026-10-01T09:30:00Z."

Sending twice: the Idempotency-Key header

Networks time out and scripts retry. To make a retry safe, send an Idempotency-Key header on POST /posts with a value that is new for each post, for example a UUID:

curl -X POST "https://app.postozo.com/public/v1/posts" \
  -H "Authorization: pz_live_YOUR_KEY" \
  -H "Idempotency-Key: 3f1c9a52-7d4e-4b8a-9c21-6e0f5d2b8a47" \
  -H "Content-Type: application/json" \
  -d '{"type": "draft", "posts": [{"integration": {"id": "de53f029-255d-4104-b090-974efcca2c86"}, "value": [{"content": "Only once"}], "settings": {}}]}'
  • The same key from the same workspace within 24 hours, with the same body, returns the first answer (the same status and body) with the header Idempotent-Replayed: true. No 2nd post is made. The order of the keys in the JSON does not matter.
  • The same key with a different body is refused: 409 "This Idempotency-Key was already used with a different request body." Use a new key for a new post.
  • If the 2nd request comes while the 1st is still running, it gets 409 "A request with this Idempotency-Key is still running." Wait a few seconds and send it again to get the answer.
  • Only answers that worked are kept. If the first request failed (for example 400 because the text is too long), fix the body and send it with the same key.
  • Keys are up to 255 characters. They are forgotten after 24 hours. Each workspace has its own keys. Replays count toward the rate limit.
  • Without the header, 2 identical requests make 2 posts. Then, after a timeout, call GET /posts for that time range and look for your text before you send it again.

The no double post rule

Publishing is different. Postozo writes down each attempt before it calls a network. If the network does not answer, the worker searches the network for the post. If it finds it, the post is marked published. If it cannot tell, the channel becomes unconfirmed (Postiz state ERROR), you get a notification and a post.unconfirmed webhook, and nothing is sent again until a person decides. Then either link the live post with PUT /posts/{id}/release-id, or retry with POST /posts/{id}/retry. GET /posts/{id}/missing asks the network for you.

Postiz compatibility

The API follows the Postiz public API: the same /public/v1 paths, the same Authorization: <key> header, the same POST /posts body and the same answer fields. To move a Postiz script or an n8n, Make or Zapier flow, change the base URL to https://app.postozo.com/public/v1 and use your Postozo key.

  • Also in Postozo: GET /posts/{id}, POST /posts/{id}/retry, PUT /posts/{id}/date, GET /find-slot without a channel, GET /networks and GET /rules/{identifier}, the review post type and a simpler post body.
  • Answers keep the Postiz fields and add a few, for example status and error with a fix in GET /posts.
  • Not in Postozo: the Postiz video generation, clipping, user and debug endpoints.
  • Delete answers {"error": false, "success": true}. The Postiz type update is read as schedule.

Next: the API reference with every endpoint, or webhooks to hear about results.

FAQ

Questions

Should I send "Bearer" before the key?

Either works. The server removes "Bearer " if it is there, so "Authorization: pz_live_..." and "Authorization: Bearer pz_live_..." are the same.

What happens if I send the same POST /posts twice?

Without a key you get 2 posts. Send an Idempotency-Key header (for example a UUID) and send the same key again when you retry: within 24 hours you get the first answer back and no 2nd post. Publishing itself never sends 1 post twice.

Why did my post go to review instead of the schedule?

The channel belongs to a customer with approval turned on, so every post for it waits for the client. Send type "now" to skip it, or turn approval off for that customer.

How do I get a new key?

In Settings, Developers, press Rotate Key. The old key stops working at once, so update your scripts first.

Which time zone do answers use?

UTC, as ISO 8601 with a Z at the end, for example 2026-10-01T04:00:00.000Z.

How long should I wait after a 429?

The number of seconds in the Retry-After header. The message also says it in minutes.

Get your API key

Start a 7-day free trial. The API, MCP server, webhooks and CLI are in every plan.

Create your free account