Server URL
https://app.postozo.com/mcp
Setup steps for each app are on the AI agents pages: Claude, Claude Code, ChatGPT, Cursor, VS Code and Windsurf. This page is the technical reference behind them.
Sign-in
| Method | How | Use it when |
|---|---|---|
| OAuth (best) | Paste the URL in your app. The app finds the sign-in details, registers itself (dynamic client registration), opens a Postozo page where you sign in and press Allow, and gets a pz_oat_ token. Details on the OAuth page. | Your app supports OAuth for remote MCP servers, like Claude and Claude Code. |
| API key as Bearer token | Send Authorization: Bearer pz_live_YOUR_KEY with every request. A CLI token (pz_cli_) works the same way. | Your app lets you add headers, like Cursor, VS Code and Claude Code. |
| Key in the URL | https://app.postozo.com/mcp/pz_live_YOUR_KEY | Only when your app can neither sign in nor send headers. |
Why the key in the URL is less safe: a URL is easy to leak. It ends up in app settings, browser history, screenshots, shared config files and the logs of proxies between you and us. Anyone with it can use your whole workspace. Postozo hides the key in its own logs, but it cannot clean the others. If you use it, never share the URL, and rotate the key in Settings, Developers if it leaks.
Without a token, the server answers 401 with a WWW-Authenticate: Bearer resource_metadata="https://app.postozo.com/.well-known/oauth-protected-resource" header. That header is how apps find the sign-in. A token that belongs to a person acts with their role in the workspace; the workspace key acts as an admin.
Transport
- Streamable HTTP, stateless. Send each JSON-RPC message as a
POSTto/mcp. There is no session: noMcp-Session-Idheader, and every request can go to any server. - The
Acceptheader must list bothapplication/jsonandtext/event-stream, or the answer is 406. Answers come back as plain JSON (application/json), not as a stream. - Notifications (messages without an
id) get 202 with no body. GETandDELETEon/mcpanswer 405 withAllow: POST, because there is no stream or session to open or close.- Protocol version 2025-06-18 is used in the examples below. The server answers with the version it agrees on in the initialize reply.
- Limit: 600 requests a minute per workspace. Over it, the answer is HTTP 429 with a Retry-After header in seconds.
- Errors before a request reaches MCP (no token, a paused team member, the limit, GET or DELETE) are JSON with
errorandmsg.
The server sends these instructions to the AI in its initialize reply:
Postozo schedules social media posts. Start with list_channels. Read get_channel_rules before posting to a network. Use find_free_slot when no time is given. Confirm text, channels and time with the user before schedule_post with type schedule or now.
Try it with curl
curl -X POST "https://app.postozo.com/mcp" \
-H "Authorization: Bearer pz_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0"}}}'
{
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {},
"prompts": {}
},
"serverInfo": {
"name": "postozo",
"version": "0.1.0"
},
"instructions": "Postozo schedules social media posts. Start with list_channels. Read get_channel_rules before posting to a network. Use find_free_slot when no time is given. Confirm text, channels and time with the user before schedule_post with type schedule or now."
},
"jsonrpc": "2.0",
"id": 1
}
Then call a tool. The result has the data twice: as JSON text in content (for the AI) and as structuredContent (lists are wrapped in { "items": [...] }). Failures come back as a normal result with isError: true and { "error": "...", "msg": "..." } (the same message twice, as in the API), plus fix when there is 1; rule problems add problems.
curl -X POST "https://app.postozo.com/mcp" \
-H "Authorization: Bearer pz_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"find_free_slot","arguments":{"channelIds":["de53f029-255d-4104-b090-974efcca2c86"],"tzOffsetMinutes":330}}}'
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"date\": \"2026-10-01T03:30:00.000Z\"\n}"
}
],
"structuredContent": {
"date": "2026-10-01T03:30:00.000Z"
},
"isError": false
},
"jsonrpc": "2.0",
"id": 2
}
find_free_slot reads each channel's posting times as local times of day. tzOffsetMinutes: 330 is India (UTC+5:30), so a posting time of 9:00 comes back as 03:30 UTC. Without tzOffsetMinutes, the time zone in the profile of the person who signed in is used (the owner's for the workspace key), or UTC. It gives the same answer as GET /find-slot in the API.
When the workspace cannot use the API
MCP follows the same rule as the public API. When the workspace has no plan (the trial ended without a plan, or a cancelled plan ended), or the 7 day grace after a failed payment is over, initialize, tools/list and the prompts still answer, so your AI app can connect and show the problem. Every tools/call is refused with the API's message and fix, and the API's HTTP status in status (401 for no plan, 402 for a failed payment):
{
"result": {
"content": [
{
"type": "text",
"text": "{ \"error\": \"No subscription found\", ... }"
}
],
"structuredContent": {
"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.",
"status": 401
},
"isError": true
},
"jsonrpc": "2.0",
"id": 3
}
Nothing is deleted. Tool calls work again as soon as the owner picks a plan or the payment goes through. During the 7 day grace everything works.
The tools
14 tools only read. The 6 that change data (schedule_post, change_post_date, retry_post, delete_post, upload_media_from_url and generate_image) have readOnlyHint: false, and delete_post also has destructiveHint: true, so your app can ask you before it runs them. This list is read from the server code when this page is built.
list_channels
List the connected social channels (accounts, pages) with their id, network, name and status. Call this first to get channel ids.
list_networks
List every network this server supports and whether it can be connected. Useful to explain what is possible.
list_customers
List customers. A customer groups channels (agencies use 1 per client).
get_channel_rules
Get the posting rules for 1 channel: max length, media needs, whether comments are supported, and the settings schema (some settings are required, for example a title on YouTube or a board on Pinterest). Read before scheduling to that channel.
| Argument | Type | Required | Description |
|---|---|---|---|
channelId | string | Yes | Channel id |
find_free_slot
Find the next free posting time for these channels, using each channel's posting times (times of day in the user's local time). Returns an ISO date in UTC.
| Argument | Type | Required | Description |
|---|---|---|---|
channelIds[] | array of string | No | Channel ids. Leave out for all channels. |
tzOffsetMinutes | number | No | The user's time zone as minutes to add to UTC to get local time: 330 for India (UTC+5:30), -300 for New York in winter (UTC-5), 0 for UTC. Leave out to use the time zone in the user's profile. |
check_post
Check a post against every channel's rules without saving it. Returns a list of problems (empty means it is fine).
| Argument | Type | Required | Description |
|---|---|---|---|
type | string | Yes | draft = save only. schedule = publish at "date". now = publish right away. review = send to the client for approval first. |
date | string | No | When to publish, ISO 8601 with timezone, for example 2026-10-01T09:30:00+05:30. Not needed for type now. Use find_free_slot to pick a good time. |
content | string | No | The main post text. Plain text. Used for every channel unless the channel entry gives its own content. |
media[] | array of string | No | Media ids from upload_media_from_url or list_media. |
thread[] | array of object | No | Follow-up comments or thread items, posted after the main post in order. |
thread[].content | string | Yes | |
thread[].media[] | array of string | No | Media ids |
thread[].delay | number | No | Minutes to wait after the previous item. |
channels[] | array of object | Yes | Where to post. Each entry needs channelId. Add content, media or settings only to change them for that channel. Read get_channel_rules first to learn the required settings. |
channels[].channelId | string | Yes | |
channels[].content | string | No | |
channels[].media[] | array of string | No | Media ids for this channel only |
channels[].settings | object | No | Network settings, keys from get_channel_rules. |
tags[] | array of string | No | Tag names. |
repeatDays | number | No | Repeat the post every N days. Leave out for no repeat. |
shortLink | boolean | No | Replace links with trackable short links. |
groupId | string | No | To edit an existing post, pass its id. |
schedule_post
Create or edit a post for 1 or more channels. Supports drafts, scheduling, posting now, client review, threads with delays, per-channel text, network settings, tags and repeats. Returns the post id, its status and a preview link.
| Argument | Type | Required | Description |
|---|---|---|---|
type | string | Yes | draft = save only. schedule = publish at "date". now = publish right away. review = send to the client for approval first. |
date | string | No | When to publish, ISO 8601 with timezone, for example 2026-10-01T09:30:00+05:30. Not needed for type now. Use find_free_slot to pick a good time. |
content | string | No | The main post text. Plain text. Used for every channel unless the channel entry gives its own content. |
media[] | array of string | No | Media ids from upload_media_from_url or list_media. |
thread[] | array of object | No | Follow-up comments or thread items, posted after the main post in order. |
thread[].content | string | Yes | |
thread[].media[] | array of string | No | Media ids |
thread[].delay | number | No | Minutes to wait after the previous item. |
channels[] | array of object | Yes | Where to post. Each entry needs channelId. Add content, media or settings only to change them for that channel. Read get_channel_rules first to learn the required settings. |
channels[].channelId | string | Yes | |
channels[].content | string | No | |
channels[].media[] | array of string | No | Media ids for this channel only |
channels[].settings | object | No | Network settings, keys from get_channel_rules. |
tags[] | array of string | No | Tag names. |
repeatDays | number | No | Repeat the post every N days. Leave out for no repeat. |
shortLink | boolean | No | Replace links with trackable short links. |
groupId | string | No | To edit an existing post, pass its id. |
list_posts
List posts on the calendar between 2 dates (default: last 7 days to next 30 days).
| Argument | Type | Required | Description |
|---|---|---|---|
start | string | No | ISO date |
end | string | No | ISO date |
customerId | string | No | Only this customer |
get_post
Get 1 post with its text per channel, state per channel (queued, published, error...), links to the live posts, errors with a fix, comments and history.
| Argument | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Post id |
change_post_date
Move a post to a new date and time.
| Argument | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Post id |
date | string | Yes | New ISO 8601 date |
retry_post
Retry the channels of a post that failed or could not be confirmed. Only use after the user confirms the post is not already live.
| Argument | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Post id |
delete_post
Delete a post from the calendar. Posts already live on a network stay live there.
| Argument | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Post id |
upload_media_from_url
Import an image or video from a public URL into the media library. Returns a media id to use in schedule_post.
| Argument | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Public http(s) URL of the file |
list_media
List files in the media library.
| Argument | Type | Required | Description |
|---|---|---|---|
search | string | No | Filter by name |
type | string | No | image or video |
list_tags
List tags used to label posts.
get_analytics
Get account statistics for 1 channel from its network (7, 30 or 90 days). Some networks do not share statistics.
| Argument | Type | Required | Description |
|---|---|---|---|
channelId | string | Yes | Channel id |
days | number | No |
get_post_analytics
Get statistics for 1 published post on 1 channel. Use the per-channel post id from get_post (channels[].root.id).
| Argument | Type | Required | Description |
|---|---|---|---|
postId | string | Yes | Per-channel post id |
get_workspace_stats
Counts from this workspace: posts by state, published posts per channel, and short link clicks.
| Argument | Type | Required | Description |
|---|---|---|---|
days | number | No |
run_channel_tool
Run a network helper for 1 channel, for example list Pinterest boards, Reddit flairs or YouTube playlists, to fill a setting. get_channel_rules lists the tools for each channel.
| Argument | Type | Required | Description |
|---|---|---|---|
channelId | string | Yes | Channel id |
tool | string | Yes | Tool name |
data | object | No | Tool input |
generate_image
Create an image with AI from a description and save it to the media library. Uses 1 image credit. Returns a media id. Only listed when the server has AI images turned on.
| Argument | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes | What the image should show |
The 2 prompts
| Prompt | What it does | Argument | Text sent to the AI |
|---|---|---|---|
plan_week | Plan a week of posts for my channels | topic (required): What the posts are about | Plan 5 posts for the next 7 days about: <topic>. List my channels first, check each network's rules, pick free slots, and show me the plan as a table. Save them as drafts only after I say yes. |
repurpose | Turn a link or text into posts for each channel | source (required): A link or the text to reuse | Turn this into 1 post per connected channel, following each network's rules: <source>. Show me the drafts before saving. |
prompts/get with a name that is not in this list gets a JSON-RPC error with code -32602 (invalid params), for example {"code": -32602, "message": "MCP error -32602: Unknown prompt nope. Use prompts/list to see the names: plan_week, repurpose."}.
Example conversations
"Schedule our autumn menu post on Instagram and Facebook tomorrow morning." A well behaved app does this:
list_channelsto find the Instagram and Facebook channel ids.get_channel_rulesfor each, and learns that Instagram needs a photo or video.- Asks you for a picture, or imports 1 with
upload_media_from_url. check_postwith the text, media and channels. An emptyproblemslist means it fits.find_free_slotwith your time zone offset when you did not give an exact time.- Shows you the post and the time, and after your yes calls
schedule_post:
{
"type": "schedule",
"date": "2026-10-01T09:30:00+05:30",
"content": "Our autumn menu is here. Pumpkin soup, apple pie and hot cider.",
"media": [
"281e94f5-fe31-40ed-8718-8dd3d1fbcd6d"
],
"channels": [
{
"channelId": "de53f029-255d-4104-b090-974efcca2c86"
},
{
"channelId": "f451da5e-2cd3-4674-bbbb-dc6bc5e12b55",
"content": "Our autumn menu is here. See you this weekend!"
}
]
}
"Why did my LinkedIn post fail yesterday?" The app calls list_posts for yesterday, then get_post, which returns each channel's error with a fix, for example "This channel is missing a permission." and "Reconnect the channel and accept all requested permissions."
"Move everything on Friday to Monday." list_posts for Friday, then change_post_date for each post, after you confirm.
Local server for Claude Desktop
Apps that only start local servers can use the postozo mcp command. It reads JSON-RPC on standard input and passes each message to https://app.postozo.com/mcp with your CLI sign-in.
{
"mcpServers": {
"postozo": {
"command": "postozo",
"args": [
"mcp"
]
}
}
}Questions
Which AI apps work?
Any app that supports remote MCP servers over HTTP. We have step by step guides for Claude, Claude Code, ChatGPT, Cursor, VS Code and Windsurf.
Can the AI publish without asking me?
The server tells the AI to confirm text, channels and time with you before it schedules or publishes, and the 6 tools that change data are marked so your app can ask before it runs them. Ask for drafts or client review when a person must check first.
Do I need an API key?
No, if your app supports OAuth sign-in: paste the server URL and sign in. Apps without OAuth can send the API key as a Bearer token.
Does the server keep a session?
No. It is stateless: every POST is handled on its own, so there is no session id and GET or DELETE on /mcp answer 405.
What about Claude Desktop with a local server?
Run postozo mcp from the CLI. It is a small local server that passes every message to the remote server, so you get the same tools.
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