# 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. **Source:** https://posteverywhere.ai/docs/integrations/claude **Section:** Integrations **API reference:** https://posteverywhere.ai/docs/api/reference --- 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. > **Easiest setup: hosted MCP, no install** > > Skip local config entirely: connect to our hosted endpoint at **`https://mcp.posteverywhere.ai`** with your API key as a Bearer header. One command in Claude Code: > > ```bash > claude mcp add --transport http posteverywhere \ > https://mcp.posteverywhere.ai \ > --header "Authorization: Bearer pe_live_YOUR_KEY" > ``` > > See [MCP Server → Hosted MCP](/docs/integrations/mcp) for every client. The `npx` configs 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`, and `draft` (optional). All timestamps are UTC. > > When building a Claude tool definition, use the exact canonical names from the [Create Post reference](/docs/api/create-post). 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](/docs/integrations/agents) for framework-agnostic patterns (Python, LangChain, OpenClaw) and the canonical agent system-prompt template in [Agent System Prompt](/docs/integrations/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_post` with `draft: true`), let you review them (`list_posts` with `status: "draft"` returns each draft's target accounts and per-platform content), then `schedule_post` once approved, passing `scheduled_for` or `publish_now: true`. `schedule_post` only works on drafts. Both tools are defined below. ## Connect Claude Code with the MCP server [Claude Code](https://docs.anthropic.com/en/docs/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: ```json { "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](/docs/integrations/mcp) for detailed instructions, troubleshooting, and configuration for other tools. ## Connect Claude Desktop with the MCP server [Claude Desktop](https://claude.ai/download) 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` ```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](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview) 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 ```bash npm install @anthropic-ai/sdk @posteverywhere/sdk ``` ### Define PostEverywhere as a Tool ```typescript 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 500 MB) 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.', }, }, 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_at` | `scheduled_for` | `scheduled_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_at` | `scheduled_for` | Never accepted. | > | `schedule_for` | `scheduled_for` | Note the **`d`** in "scheduled". | > | `media`, `media_id`, `mediaId`, `attachments` | `media_ids` | All rejected. The only accepted name is `media_ids` (array of UUID strings from the upload flow). | > | `accountIds` | `account_ids` | camelCase 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 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. > > ```json > { > "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 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](/docs/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. ```typescript // 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 ```typescript async function handleToolCall( name: string, input: Record ): Promise { 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 ); 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 ```typescript 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](/docs/integrations/agent-system-prompt): Drop-in system prompt for any Claude agent that calls PostEverywhere - [Building AI Agents](/docs/integrations/agents): Framework-agnostic patterns (Python, LangChain, OpenClaw) - [MCP Server Setup](/docs/integrations/mcp): Detailed MCP configuration and troubleshooting - [SDKs & CLI](/docs/integrations/sdk): Node.js SDK and CLI reference - [Create Post API](/docs/api/create-post): Full endpoint documentation - [Rate Limits](/docs/rate-limits): Request limits and retry strategies **Related:** [AI agents on PostEverywhere](/agents)