Skip to main content

Using PostEverywhere with Claude

There are three ways to use Claude with PostEverywhere, depending on your use case: MCP for interactive use in Claude Code or Claude Desktop, or the Anthropic API for building your own automated agent.

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. 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 — 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.

Option 1: Claude Code (MCP)

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.

Option 2: Claude Desktop (MCP)

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.

Option 3: Claude API (Build Your Own Agent)

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 Dependencies

npm install @anthropic-ai/sdk posteverywhere

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. Image-only (JPEG, PNG, GIF, WebP, ' +
'HEIC, HEIF), 25 MB cap. For local files or videos, use upload_media (the 3-step flow) instead.',
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 the media object with status, dimensions, url, and aspect_ratio. ' +
'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. ' +
'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.',
},
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.',
},
},
required: ['content', 'account_ids'],
},
},
];
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 all of these with 400 invalid_field_name:

What Claude inventsCorrect fieldNotes
scheduled_atscheduled_forscheduled_at is a deprecated alias — still silently accepted, but responses always return scheduled_for. Never describe scheduled_at as a tool property. New integrations must use scheduled_for.
schedule_time, publish_at, post_atscheduled_forNever accepted.
schedule_forscheduled_forNote the d in "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/postsvalidation_warnings: When media doesn't perfectly match a platform's requirements (e.g. aspect ratio outside optimal range), the response includes a validation_warnings object 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/:idurl: The media status response includes a permanent url field 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.
await fetch(upload.uploadUrl, {
method: 'PUT',
body: fileBytes,
headers: { 'Content-Type': 'image/jpeg' },
});

// Step 3 — Finalize (confirms file was received, returns url + dimensions)
const media = await handleToolCall('complete_media', {
media_id: upload.mediaId,
});
// 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.upload({
fileName: input.filename,
contentType: input.content_type,
size: input.size,
});
return JSON.stringify(upload);
// Next: upload file bytes to upload.uploadUrl, then call complete_media
}
case 'complete_media': {
const media = await pe.media.complete(input.media_id);
// Response includes: media_id, media_status, url, dimensions, aspect_ratio
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
});
// Response may include validation_warnings (non-blocking) when media
// doesn't perfectly match platform requirements (e.g. aspect ratio).
return JSON.stringify(post);
}
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-4-20250514',
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 Workflow: A Week of Content

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. Eight platforms. 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.

Next Steps