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
| Token | Starts with | Where it comes from | Acts as |
|---|---|---|---|
| Workspace API key | pz_live_ | Settings, Developers (owners and admins only). 1 key per workspace. | An admin of that workspace. Posts show "API" as the author. |
| OAuth token | pz_oat_ | An app the person approved with OAuth. | The person who approved, with their role in that workspace. |
| CLI token | pz_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 exampleno_subscription,payment_failed,idempotency_key_reused,email_not_verifiedormember_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."
}
]
}
| Status | When |
|---|---|
| 400 | The 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). |
| 402 | A 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. |
| 403 | Your 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. |
| 404 | The post, channel, media or network does not exist in this workspace. |
| 413 | The 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. |
| 409 | The same Idempotency-Key with a different body (or while the first request is still running), or something with the same unique value already exists. |
| 429 | Too many requests. The Retry-After header gives the seconds to wait. See the limits below. |
| 500 | Something 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:
| Status | When | Body |
|---|---|---|
| 401 | No Authorization header. | {"error":"No API Key found","msg":"No API Key found"} |
| 401 | The key or token is wrong, was rotated or was revoked. | {"error":"Invalid API key","msg":"Invalid API key"} |
| 400 | The body is not valid JSON. | {"error":"The request body is not valid JSON.","msg":"The request body is not valid JSON."} |
| 404 | The post, channel or media does not exist in this workspace. | {"error":"Post not found","msg":"Post not found"} |
| 400 | An 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."} |
| 404 | The path does not exist. | {"error":"Not found","msg":"Not found","fix":"Check the method and the path. The API reference lists every path."} |
| 401 | The 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."} |
| 402 | Publishing 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."} |
| 409 | Something 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."} |
| 429 | Over 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."} |
| 500 | Something 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
| What | Limit | Counted per |
|---|---|---|
POST /public/v1/posts | 90 requests an hour | Workspace |
MCP (/mcp) | 600 requests a minute | Workspace |
POST /oauth/register | 30 an hour | IP address |
POST /oauth/token | 300 an hour | IP address |
POST /oauth/device/code | 30 an hour | IP address |
| All other API endpoints | No 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 /postshas no pages. It returns every post betweenstartDateandendDate(default: 30 days back to 30 days ahead). Ask for shorter ranges to get fewer rows.GET /mediareturns 40 files a page. Sendpage=2,page=3and so on. A page with fewer than 40 files is the last.GET /notificationsreturns 50 a page with the samepageparameter, and echoes the page number.GET /integrations,GET /groupsandGET /networksreturn everything at once.
Dates and time zones
- Send dates as ISO 8601 with a time zone:
2026-10-01T09:30:00+05:30or2026-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
scheduleorreviewpost cannot be more than 1 minute in the past. Drafts can have any date.nowignores the date. - The free slot endpoints (
GET /find-slotand the MCP toolfind_free_slot) read each channel's posting times (set in the app) as local times of day. SendtzOffsetMinutes, the minutes to add to UTC to get your local time (330for India,-300for 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 as03: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 /postsfor 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-slotwithout a channel,GET /networksandGET /rules/{identifier}, thereviewpost type and a simpler post body. - Answers keep the Postiz fields and add a few, for example
statusanderrorwith a fix inGET /posts. - Not in Postozo: the Postiz video generation, clipping, user and debug endpoints.
- Delete answers
{"error": false, "success": true}. The Postiz typeupdateis read asschedule.
Next: the API reference with every endpoint, or webhooks to hear about results.
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