PostEverywhere Claude Guide
Schedule and publish social media posts using Claude AI. Connect PostEverywhere to Claude Code, Claude Desktop, and Claude API for AI social media management.
There are three ways to use Claude with PostEverywhere, depending on your use case: add it from Claude's connector directory on claude.ai or Claude Desktop (sign in, no API key), connect the MCP server in Claude Code, or call the Anthropic API to build your own automated agent.
Easiest setup: hosted MCP, no install
Skip local config entirely: connect to our hosted endpoint at
https://mcp.posteverywhere.aiwith your API key as a Bearer header. One command in Claude Code:claude mcp add --transport http posteverywhere \ https://mcp.posteverywhere.ai \ --header "Authorization: Bearer pe_live_YOUR_KEY"See MCP Server → Hosted MCP for every client. The
npxconfigs below are the local alternative.
The round-trip principle
Every top-level field name in a PostEverywhere request body also appears in the response with the same name and meaning. You can take a post you got from
GET /v1/posts/{id}and POST its fields straight back to clone it: no renaming required. The canonical request fields are:content,account_ids,scheduled_for,timezone,media_ids,platform_content, anddraft(optional). All timestamps are UTC.When building a Claude tool definition, use the exact canonical names from the Create Post reference. Claude will happily invent plausible-but-wrong field names (
scheduled_at,media,mediaId,attachments) if you leave the tool schema vague, so be explicit.
Building a full agent?
Read Building AI Agents for framework-agnostic patterns (Python, LangChain, OpenClaw) and the canonical agent system-prompt template in Agent System Prompt.
Draft first, schedule after approval (human-in-the-loop)
For a review step before anything publishes, have Claude create posts as drafts (
create_postwithdraft: true), let you review them (list_postswithstatus: "draft"returns each draft's target accounts and per-platform content), thenschedule_postonce approved, passingscheduled_fororpublish_now: true.schedule_postonly works on drafts. Both tools are defined below.
Add PostEverywhere from Claude's connector directory
The fastest way on claude.ai and Claude Desktop. PostEverywhere is listed in Claude's connector directory: signed in to Claude, open claude.ai/directory/posteverywhere, add it, then log in to PostEverywhere and approve access. There is no config file and no API key. To add it by URL instead, follow the claude.ai steps in the MCP Server setup guide.
Connect Claude Code with the MCP server
Claude Code is Anthropic's CLI tool for agentic coding. With the PostEverywhere MCP server, Claude Code can manage your social media directly from your terminal.
Add this to your Claude Code MCP config:
{
"mcpServers": {
"posteverywhere": {
"command": "npx",
"args": ["-y", "@posteverywhere/mcp"],
"env": {
"POSTEVERYWHERE_API_KEY": "pe_live_..."
}
}
}
}
Then ask Claude to manage your social media in natural language:
"Schedule a LinkedIn post for tomorrow at 9am announcing our Series A funding"
See the full MCP Server setup guide for detailed instructions, troubleshooting, and configuration for other tools.
Through MCP, Claude can also read your Brand Kit with get_brand and your best times with get_best_times, post threads with thread_posts, take the next queue slot with use_queue: true, schedule with schedule_at: "best_time", set a video cover with cover_photo, put posts in a campaign with campaign_id, send drafts for review with request_approval: true (reviewers use approve_post and request_post_changes), and upload a local file, such as a PDF for a LinkedIn document post, with create_media_upload and complete_media_upload (in apps that can send files, like Claude Code). The full server has 44 tools; Claude's connector directory version has 43 (no generate_image). For example:
"Upload this image as my thumbnail, then post my video to YouTube with it as the cover photo."
The MCP guide lists these options.
Connect Claude Desktop with the MCP server
Claude Desktop supports MCP servers through its configuration file. This lets you manage social media from the Claude chat interface without any coding.
macOS: Edit ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: Edit %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"posteverywhere": {
"command": "npx",
"args": ["-y", "@posteverywhere/mcp"],
"env": {
"POSTEVERYWHERE_API_KEY": "pe_live_..."
}
}
}
}
Restart Claude Desktop after saving. You'll see a tools icon in the chat input indicating the MCP server is connected. Now you can chat with Claude and ask it to schedule posts, check account status, or review publishing results.
Build your own agent on the Claude API
Use the Anthropic SDK with tool use to build a custom agent that manages social media automatically. This is ideal for workflows like automated content pipelines, scheduled reporting, or AI-driven social media strategies.
Install the Anthropic and PostEverywhere SDKs
npm install @anthropic-ai/sdk @posteverywhere/sdk
Define PostEverywhere as a Tool
import Anthropic from '@anthropic-ai/sdk';
import { PostEverywhere } from '@posteverywhere/sdk';
const anthropic = new Anthropic();
const pe = new PostEverywhere({ apiKey: process.env.POSTEVERYWHERE_API_KEY });
const tools: Anthropic.Tool[] = [
{
name: 'list_accounts',
description: 'List all connected social media accounts',
input_schema: {
type: 'object' as const,
properties: {},
required: [],
},
},
{
name: 'upload_media_from_url',
description:
'PREFERRED for images already at a public URL. Fetches the URL server-side and stores it ' +
'in the PostEverywhere media library. Returns a ready-to-use media_id you can pass directly ' +
'to create_post via media_ids — no separate complete step. Images (JPEG, PNG, GIF, WebP, HEIC, ' +
'HEIF, 25 MB) are ready immediately; MP4 videos (up to 4 GB) import asynchronously — poll ' +
'get_media until media_status is "ready" before attaching. For local files, use upload_media (the 3-step flow).',
input_schema: {
type: 'object' as const,
properties: {
url: {
type: 'string',
description: 'Public HTTPS URL pointing to the image. Must be reachable from the public internet.',
},
filename: {
type: 'string',
description: 'Optional filename to record in the library. If omitted, derived from the URL path.',
},
},
required: ['url'],
},
},
{
name: 'upload_media',
description:
'Upload a media file (image or video) to the PostEverywhere media library using the 3-step ' +
'flow — use this for local files or videos. (If the image is already at a public URL, ' +
'prefer upload_media_from_url which is a single call.) ' +
'Returns an upload_url and a media_id. You must upload the file bytes to the ' +
'upload_url, then call complete_media to finalize. Only after completing can you ' +
'pass the media_id into create_post via the media_ids array.',
input_schema: {
type: 'object' as const,
properties: {
filename: {
type: 'string',
description: 'Original filename including extension, e.g. "photo.jpg".',
},
content_type: {
type: 'string',
description: 'MIME type, e.g. "image/jpeg", "video/mp4".',
},
size: {
type: 'integer',
description: 'File size in bytes.',
},
},
required: ['filename', 'content_type', 'size'],
},
},
{
name: 'complete_media',
description:
'Finalize a media upload after uploading the file to the presigned URL. ' +
'Call this AFTER uploading bytes to the upload_url from upload_media. ' +
'Returns media_id and media_status; call GET /v1/media/{id} for url and dimensions. ' +
'The media_id can then be used in media_ids when creating a post.',
input_schema: {
type: 'object' as const,
properties: {
media_id: {
type: 'string',
description: 'The UUID returned by upload_media.',
},
},
required: ['media_id'],
},
},
{
name: 'create_post',
description:
'Create and schedule a social media post. Omit scheduled_for to publish immediately. ' +
'Set draft: true to save it as a draft instead (then schedule it later with schedule_post). ' +
'All timestamps are UTC. The response has the same top-level fields as the request (round-trip).',
input_schema: {
type: 'object' as const,
properties: {
content: {
type: 'string',
description: 'The post text content.',
},
account_ids: {
type: 'array',
items: { type: 'integer' },
description:
'Account IDs (integers) to post to. Get them from list_accounts. ' +
'These are NOT usernames or strings — always integers like 2280. ' +
'Required unless draft is true (drafts can pick targets later).',
},
scheduled_for: {
type: 'string',
description:
'ISO 8601 datetime in UTC when the post should publish, e.g. "2026-04-15T14:30:00Z". ' +
'Omit to publish immediately. Always send UTC — convert from local time in your code if needed. ' +
'Do NOT use scheduled_at (that is a deprecated alias).',
},
media_ids: {
type: 'array',
items: { type: 'string' },
description:
'Optional array of media UUIDs returned from upload_media_from_url (preferred for URLs) or upload_media (for local files). ' +
'The only accepted media field name is media_ids — NOT media, media_id, mediaId, or attachments.',
},
draft: {
type: 'boolean',
description:
'Optional. Set true to save the post as a DRAFT — it is neither published nor scheduled. ' +
'account_ids is optional when draft is true. Schedule it later with the schedule_post tool. ' +
'Use this for human-in-the-loop review: draft, let the user approve, then schedule.',
},
thread_posts: {
type: 'array',
items: { type: 'string' },
description:
'Optional. Makes the post a thread: the parts that come AFTER content, in order (up to 25). ' +
'A linked thread on X and Bluesky; follow-up messages on Telegram and Discord. ' +
'Other accounts get content only. Each part must fit the platform limit (X 280, Threads 500, Bluesky 300).',
},
cover_photo: {
type: 'object',
properties: { media_id: { type: 'string' }, url: { type: 'string' } },
description:
'Optional cover for a VIDEO post: { media_id } of an uploaded image, or { url } of a public JPG/PNG. ' +
'YouTube custom thumbnail (verified channels only, 1280x720 best), Instagram Reel cover, Facebook and LinkedIn video thumbnail. ' +
'TikTok ignores it (set platform_content.tiktok.settings.videoCoverTimestamp in ms instead).',
},
use_queue: {
type: 'boolean',
description: 'Optional. Set true to let the workspace posting queue pick the next free slot. Do not send with scheduled_for.',
},
},
required: ['content'],
},
},
{
name: 'schedule_post',
description:
'Schedule (or immediately publish) an existing DRAFT post. ' +
'Provide scheduled_for to schedule, or publish_now: true to publish right now. ' +
'Only works on drafts — calling it on an already-scheduled or published post returns a 409. ' +
'Use after create_post with draft: true once the draft has been reviewed/approved.',
input_schema: {
type: 'object' as const,
properties: {
post_id: {
type: 'string',
description: 'The post_id of the draft to schedule (from create_post or list_posts).',
},
scheduled_for: {
type: 'string',
description:
'ISO 8601 UTC datetime to schedule the draft for, e.g. "2026-04-15T14:30:00Z". ' +
'Provide this OR publish_now — not both.',
},
publish_now: {
type: 'boolean',
description: 'Set true to publish the draft immediately instead of scheduling. Provide this OR scheduled_for.',
},
account_ids: {
type: 'array',
items: { type: 'integer' },
description:
'Optional. Target account IDs (integers). Sets or overrides the draft’s targets. ' +
'Required only if the draft was created without account_ids.',
},
timezone: {
type: 'string',
description: 'Optional IANA timezone for display only (e.g. "America/New_York"). Does not change when the post fires.',
},
},
required: ['post_id'],
},
},
];
Field-name pitfalls Claude tends to invent
Claude (and most other LLMs) will guess plausible-but-wrong names if the tool schema is loose. The API rejects the media aliases with
400 invalid_field_name; the others are accepted as aliases or silently ignored, which is worse, because a mistyped schedule field publishes immediately:
What Claude invents Correct field Notes scheduled_atscheduled_forscheduled_atis a deprecated alias: still silently accepted, but responses always returnscheduled_for. Never describescheduled_atas a tool property. New integrations must usescheduled_for.schedule_time,publish_at,post_atscheduled_forNever accepted. schedule_forscheduled_forNote the din "scheduled".media,media_id,mediaId,attachmentsmedia_idsAll rejected. The only accepted name is media_ids(array of UUID strings from the upload flow).accountIdsaccount_idscamelCase aliases exist for back-compat but prefer snake_case account_ids.Spell out these pitfalls in your tool description so Claude stays on the rails.
New response fields to handle
POST /v1/posts,validation_warnings: When media doesn't perfectly match a platform's requirements (e.g. aspect ratio outside optimal range), the response includes avalidation_warningsobject keyed by platform. The post still publishes, but the platform may crop or resize. Your agent should surface these to the user so they can improve future content.{ "data": { "post_id": "...", "status": "scheduled", "validation_warnings": { "instagram": "Image aspect ratio 2.5:1 is outside Instagram's supported range (4:5 to 1.91:1). The image will be cropped." } } }
GET /v1/media/:id,url: The media status response includes a permanenturlfield with a viewable/downloadable URL. URLs never expire: safe for scheduled posts.
Storage quotas and rate limits
Upload rate limits and storage quotas are enforced per plan. Handle 429 and 403 responses. See Rate Limits for headers and retry strategies. Build exponential backoff into your tool handler.
The 3-Step Media Upload Flow
Attaching media to a post requires three steps: upload, transfer bytes, complete.
// Step 1 — Start the upload (get a presigned URL)
const upload = await handleToolCall('upload_media', {
filename: 'product-launch.jpg',
content_type: 'image/jpeg',
size: 2048576,
});
// Step 2 — Upload file bytes to the presigned URL
// upload_media returns upload_url and upload_method.
// Images use multipart POST (field: "file"), videos use PUT with Content-Type.
const form = new FormData();
form.append('file', new Blob([fileBytes], { type: 'image/jpeg' }), 'product-launch.jpg');
await fetch(upload.upload_url, { method: 'POST', body: form }); // images: multipart POST · videos: PUT
// Step 3 — Finalize (confirms file was received, returns url + dimensions)
const media = await handleToolCall('complete_media', {
media_id: upload.media_id,
});
// media.url → viewable URL, media.media_status → "ready"
// Now use it in a post
await handleToolCall('create_post', {
content: 'Check this out!',
account_ids: [123],
media_ids: [media.media_id],
});
Handle Tool Calls in a Loop
async function handleToolCall(
name: string,
input: Record<string, any>
): Promise<string> {
switch (name) {
case 'list_accounts': {
const accounts = await pe.accounts.list();
return JSON.stringify(accounts);
}
case 'upload_media_from_url': {
// One-call import — server fetches the URL, stores it, returns a ready media_id.
const media = await pe.media.uploadFromUrl({
url: input.url,
filename: input.filename,
});
return JSON.stringify(media);
}
case 'upload_media': {
const upload = await pe.media.getUploadUrl({
filename: input.filename,
content_type: input.content_type,
file_size: input.size,
});
return JSON.stringify(upload);
// Next: upload file bytes to upload.upload_url, then call complete_media
}
case 'complete_media': {
const media = await pe.media.complete(input.media_id);
// Response includes: media_id, media_status (call GET /v1/media/{id} for url and dimensions)
return JSON.stringify(media);
}
case 'create_post': {
const post = await pe.posts.create({
content: input.content,
account_ids: input.account_ids,
scheduled_for: input.scheduled_for, // undefined → publish immediately
media_ids: input.media_ids, // undefined → text-only post
draft: input.draft, // true → save as draft (API-supported; SDK 1.4.2 types lag, cast if TS complains)
});
// Response may include validation_warnings (non-blocking) when media
// doesn't perfectly match platform requirements (e.g. aspect ratio).
// For a draft, post.status === 'draft' and post.next_steps points at schedule.
return JSON.stringify(post);
}
case 'schedule_post': {
// Turns an existing draft into a scheduled (or immediately-publishing) post
// via POST /v1/posts/{id}/schedule. Send scheduled_for OR publish_now.
const res = await fetch(
`https://app.posteverywhere.ai/api/v1/posts/${input.post_id}/schedule`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTEVERYWHERE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
scheduled_for: input.scheduled_for, // OR publish_now: true
publish_now: input.publish_now,
account_ids: input.account_ids, // optional: set/override targets
timezone: input.timezone, // optional: display only
}),
},
);
return JSON.stringify(await res.json());
}
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
}
async function runAgent(userMessage: string) {
const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: userMessage },
];
while (true) {
const response = await anthropic.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
tools,
messages,
});
// If Claude is done (no more tool calls), return the text
if (response.stop_reason === 'end_turn') {
const textBlock = response.content.find((b) => b.type === 'text');
return textBlock ? textBlock.text : '';
}
// Process tool calls
const assistantContent = response.content;
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of assistantContent) {
if (block.type === 'tool_use') {
const result = await handleToolCall(
block.name,
block.input as Record<string, any>
);
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: result,
});
}
}
messages.push({ role: 'assistant', content: assistantContent });
messages.push({ role: 'user', content: toolResults });
}
}
Run It
const result = await runAgent(
'List my social accounts, then schedule a post to all active Instagram ' +
'and LinkedIn accounts for tomorrow at 10am saying "Big announcement coming soon!"'
);
console.log(result);
The agent will call list_accounts to discover your connected accounts, filter for active Instagram and LinkedIn accounts, then call create_post with the right account IDs and schedule time.
Example: planning a week of content with Claude
Whether using MCP or the API, Claude can help you plan and schedule an entire week of social media content. Here's an example conversation with Claude via MCP:
You:
Create a week of social media posts for my SaaS product launch. Post to LinkedIn and X every weekday next week at 9am EST. Day 1: teaser, Day 2: problem we solve, Day 3: feature highlight, Day 4: customer quote, Day 5: launch announcement with link to example.com/launch.
Claude:
I'll create 5 posts scheduled across next week. Let me first check your connected accounts...
I found your LinkedIn account (@yourcompany) and X account (@yourcompany). Here's what I've scheduled:
- Mon 9am: "Something big is coming next week. Stay tuned." (LinkedIn + X)
- Tue 9am: "Teams waste 4 hours/week switching between social media platforms..." (LinkedIn + X)
- Wed 9am: "One API call. Every platform. Zero context-switching..." (LinkedIn + X)
- Thu 9am: "'PostEverywhere cut our social media publishing time by 80%' (Sarah, Head of Marketing at Acme)" (LinkedIn + X)
- Fri 9am: "It's here. PostEverywhere is now live..." (LinkedIn + X)
All 5 posts are scheduled. You can review or edit them in your PostEverywhere dashboard.
Where to go after connecting Claude
- Agent System Prompt: Drop-in system prompt for any Claude agent that calls PostEverywhere
- Building AI Agents: Framework-agnostic patterns (Python, LangChain, OpenClaw)
- MCP Server Setup: Detailed MCP configuration and troubleshooting
- SDKs & CLI: Node.js SDK and CLI reference
- Create Post API: Full endpoint documentation
- Rate Limits: Request limits and retry strategies
Related: AI agents on PostEverywhere