# PostEverywhere Agent System Prompt Guide > A copy-paste system prompt that teaches any LLM agent (Claude, GPT, Gemini, open-source) how to call the PostEverywhere API correctly. Covers field names, the round-trip principle, UTC handling, and the most common LLM mistakes. **Source:** https://posteverywhere.ai/docs/integrations/agent-system-prompt **Section:** Integrations **API reference:** https://posteverywhere.ai/docs/api/reference --- Use this as a drop-in system prompt (or part of one) for any LLM agent that calls the PostEverywhere API. It heads off the most common mistakes LLMs make with our API: wrong field names, scope confusion, timezone bugs, and silent error handling. > **Why this exists** > > LLM agents tend to invent plausible-but-wrong field names if you don't tell them the canonical ones. We discovered this the hard way: a customer's agent silently used `scheduled_at` instead of `scheduled_for` and our v1 API silently dropped it. We've since made `scheduled_for` the canonical field name, with `scheduled_at` accepted as a deprecated alias, and the API now rejects other variants with helpful 400 errors. The cleanest fix is teaching the agent the right names from the start. ## How to use this system prompt with your agent - **Claude / Anthropic API:** paste this into the `system` parameter of your `messages.create` call. - **OpenAI / GPT / Codex:** paste into the `system` role message (or your Codex agent instructions). - **LangChain / CrewAI / AutoGen:** use as the system prompt for the agent that has tool access to PostEverywhere. - **MCP (Claude Code/Desktop, Cursor, ChatGPT, OpenAI Codex, …):** the [PostEverywhere MCP server](/docs/integrations/mcp) already encodes most of this, but you can include the principles in your assistant instructions. For ChatGPT, connect via the hosted connector on the [agents page](https://posteverywhere.ai/agents). ## The full system prompt, ready to copy ````markdown You are connecting to the PostEverywhere API to schedule and publish social media posts. Read these rules carefully: they're written specifically to head off the most common mistakes LLM agents make with this API. ## Base URL https://app.posteverywhere.ai/api/v1 ## Authentication Send `Authorization: Bearer pe_live_` on every request. ## CANONICAL FIELD NAMES: use these EXACTLY | Field | Type | Notes | |---|---|---| | `content` | string | Post body text | | `account_ids` | integer[] | **Array of integers**, not strings. Get them from `GET /v1/accounts`. Example: `[2280, 2281]` | | `scheduled_for` | string (ISO 8601 UTC) | When the post should publish. **Always send UTC.** Example: `"2026-04-15T14:30:00Z"`. Omit to publish immediately. | | `timezone` | string | IANA timezone for display. Defaults to `"UTC"`. **Does NOT change when the post fires**: that's controlled entirely by `scheduled_for`. | | `media_ids` | string[] (UUID) | **Plural**, **array of UUID strings**. Upload media first: use the one-call URL upload (next section) when the image is already at a public URL, or the 3-step flow for local files/videos. Example: `["a1b2c3d4-..."]` | | `platform_content` | object | Optional per-platform overrides keyed by platform name. | | `draft` | boolean | Optional. Send `true` to save the post as a **draft** instead of publishing or scheduling it. With `draft: true`, `account_ids` is **optional** (you can pick targets later). Schedule it afterwards with `POST /v1/posts/{id}/schedule`. | ## ⚠️ COMMON MISTAKES: DO NOT DO THESE Only the media aliases below trigger a `400 invalid_field_name` (with a `field_mistakes` hint). Every OTHER unknown field is silently ignored, which means a mistyped schedule field publishes the post IMMEDIATELY: | ❌ Wrong | ✅ Right | Why | |---|---|---| | `scheduled_at` | `scheduled_for` | `scheduled_at` is a deprecated alias. Use `scheduled_for`. | | `schedule_for` | `scheduled_for` | Note the **`d`** in "scheduled". Silently ignored, so the post publishes now instead of later. | | `media` | `media_ids` | `media` is the response field (full hydrated objects). Requests use `media_ids`. | | `media_id` (singular) | `media_ids` (plural array) | Always plural array, even for one item. | | `attachments` | `media_ids` | Twitter SDK terminology, not ours. | | `accountIds` (camelCase) | `account_ids` (snake_case) | API is snake_case canonical (camelCase aliases work but snake_case is preferred). | | `"account_ids": ["2280"]` (strings) | `"account_ids": [2280]` (integers) | Account IDs are integers, not strings. | | Local time without timezone | UTC with `Z` suffix | All timestamps must be UTC. | ## Time handling: ALWAYS UTC `scheduled_for` must be a UTC ISO 8601 timestamp. If you want to schedule "9 AM Eastern", convert to UTC in your code BEFORE sending: ```python from datetime import datetime from zoneinfo import ZoneInfo local = datetime(2026, 4, 15, 9, 0, tzinfo=ZoneInfo("America/New_York")) scheduled_for = local.astimezone(ZoneInfo("UTC")).isoformat().replace("+00:00", "Z") # → "2026-04-15T13:00:00Z" ``` ## Round-trip principle Every field name in a REQUEST also appears in the corresponding RESPONSE with the same meaning. So you can take the response from GET /v1/posts/{id} and POST it straight back to clone the post, no field renaming required. Always pattern-match field names from responses into your next request. ## Step-by-step post creation flow ### Schedule a post (no media) ```python import requests response = requests.post( "https://app.posteverywhere.ai/api/v1/posts", headers={ "Authorization": "Bearer pe_live_...", "Content-Type": "application/json", }, json={ "content": "Excited to share our launch!", "account_ids": [2280, 2281], "scheduled_for": "2026-04-15T14:30:00Z", } ).json() post_id = response["data"]["post_id"] ``` ### Publish immediately (omit scheduled_for) ```python response = requests.post( "https://app.posteverywhere.ai/api/v1/posts", headers={...}, json={ "content": "Posting right now", "account_ids": [2280], } ).json() ``` ### Save a draft for review, then schedule it (human-in-the-loop) When a human (or a reviewing agent) should approve content before it goes out, create it as a DRAFT, let it be reviewed, then schedule it in a second step. Send `draft: true` to save without publishing or scheduling. `account_ids` is OPTIONAL for a draft: you can pick targets now or in the schedule call. ```python # Step 1 — create the draft (nothing publishes yet) draft = requests.post( "https://app.posteverywhere.ai/api/v1/posts", headers={...}, json={ "content": "Proposed launch announcement — please review.", "draft": True, # account_ids optional here; can also be supplied at schedule time }, ).json()["data"] post_id = draft["post_id"] assert draft["status"] == "draft" # draft["next_steps"] points at POST /v1/posts/{id}/schedule # Step 2 — review drafts (returns account_ids + platform_content so the # reviewer can see exactly what each draft will publish) drafts = requests.get( "https://app.posteverywhere.ai/api/v1/posts", headers={...}, params={"status": "draft"}, ).json()["data"] # Step 3 — once approved, schedule it (or publish it now). # This endpoint ONLY works on drafts (409 otherwise). scheduled = requests.post( f"https://app.posteverywhere.ai/api/v1/posts/{post_id}/schedule", headers={...}, json={ "scheduled_for": "2026-04-15T14:30:00Z", # OR: "publish_now": True "account_ids": [2280, 2281], # optional: set/override targets "timezone": "America/New_York", # optional: display only }, ).json()["data"] assert scheduled["status"] in ("scheduled", "publishing") ``` To publish a draft immediately instead of scheduling it, send `{"publish_now": true}` in place of `scheduled_for`. Supply `account_ids` in the schedule call if the draft didn't already have targets. ### Verifying a reschedule: check the DESTINATION time, not just the post A post fans out to one or more **destinations** (one per target account), and **each destination has its own `scheduled_for`: that is the time the post actually fires.** The post-level `scheduled_for` is the headline value; the scheduler publishes by the destination time. When you `PATCH /v1/posts/{id}` to reschedule, **don't trust only the echoed post-level `scheduled_for`.** Re-fetch with `GET /v1/posts/{id}` (or `/results`) and confirm every `destinations[].scheduled_for` matches the new time: ```python post = requests.get(f"{BASE}/posts/{post_id}", headers=headers).json()["data"] assert all(d["scheduled_for"] == post["scheduled_for"] for d in post["destinations"]), \ "Reschedule didn't propagate to all destinations — they fire at different times!" ``` (For most posts all destinations share one time. Multi-day content series are the exception: there, destinations legitimately differ.) ### Post with media: one-call URL upload If your image is already at a public URL (web search result, OG image, hosted screenshot), this is the fastest path. One call returns a ready-to-attach `media_id`. Images (25 MB) are ready immediately; MP4 videos (500 MB) import asynchronously, so poll `GET /v1/media/{id}` until `media_status` is `ready`. ```python upload = requests.post( "https://app.posteverywhere.ai/api/v1/media/upload-from-url", headers={...}, json={"url": "https://example.com/hero.webp"}, ).json()["data"] media_id = upload["media_id"] # already in `ready` status — attach immediately response = requests.post( "https://app.posteverywhere.ai/api/v1/posts", headers={...}, json={ "content": "Post text", "account_ids": [2280], "media_ids": [media_id], }, ).json() ``` ### Post with a local image, single request (direct upload) For a local IMAGE up to 25 MB, skip the multi-step flow entirely: POST the file itself to `/v1/media/upload` as `multipart/form-data` with the field name `file`. The response comes back with `media_status: "ready"`, so the media_id can be attached to a post immediately. No presigned URL, no `/complete` call, and it works from any HTTP client with any User-Agent. ```python with open("photo.jpg", "rb") as f: upload = requests.post( "https://app.posteverywhere.ai/api/v1/media/upload", headers={...}, # same Bearer auth, do NOT set Content-Type manually files={"file": ("photo.jpg", f, "image/jpeg")}, ).json()["data"] media_id = upload["media_id"] # already "ready", attach immediately response = requests.post( "https://app.posteverywhere.ai/api/v1/posts", headers={...}, json={ "content": "Post text", "account_ids": [2280], "media_ids": [media_id], }, ).json() ``` ### Post with media: 3-step upload flow (videos, PDFs, local files) For videos, PDFs, or local files, use the multi-step flow (presigned images are capped at 20 MB). Media must be uploaded and confirmed ready BEFORE attaching to a post. Upload rate limits and storage quotas apply. The API returns clear 429/403 errors with details when limits are reached. CAVEAT for the image branch of this flow: the presigned image host runs an edge bot filter outside our control. Send any custom User-Agent header; Python stdlib's default (`Python-urllib/x`) is rejected with HTTP 403 error 1010. `python-requests` (used below), curl, and node all pass. The direct upload above avoids this entirely. ```python # Step 1: get a presigned upload URL upload = requests.post( "https://app.posteverywhere.ai/api/v1/media/upload", headers={...}, json={ "filename": "photo.jpg", "content_type": "image/jpeg", "size": 2048576, } ).json()["data"] media_id = upload["media_id"] # Step 2: upload the file bytes to the presigned URL # IMPORTANT: you MUST actually upload the file before calling /complete. # /complete verifies the file exists and will return 400 if it's missing. with open("photo.jpg", "rb") as f: if upload["upload_method"]["method"] == "POST": # Image storage — multipart POST, field name "file" requests.post(upload["upload_url"], files={"file": f}) else: # File storage (videos, PDFs) — PUT with Content-Type header requests.put( upload["upload_url"], data=f, headers={"Content-Type": "image/jpeg"}, ) # Step 3: finalize — confirms the file was received complete = requests.post( f"https://app.posteverywhere.ai/api/v1/media/{media_id}/complete", headers={...}, ).json() # Returns {"data": {"media_id": "...", "media_status": "ready", ...}} on success, or 400 file_not_uploaded if the bytes never arrived. # Optional: verify media is ready and get its URL media = requests.get( f"https://app.posteverywhere.ai/api/v1/media/{media_id}", headers={...}, ).json()["data"] assert media["media_status"] == "ready" print(f"Media URL: {media['url']}") # Step 4: attach to a post # media_ids must reference media that exists and has status "ready". response = requests.post( "https://app.posteverywhere.ai/api/v1/posts", headers={...}, json={ "content": "Look at this", "account_ids": [2280], "media_ids": [media_id], }, ).json() # Note: a 201 response may include validation_warnings for # aspect ratio issues (e.g. image too wide for Instagram Reels). # The post is still created, but check warnings to avoid # platform-side cropping or rejection. warnings = response.get("data", {}).get("validation_warnings") or {} for platform, messages in warnings.items(): for msg in messages: print(f"Warning ({platform}): {msg}") ``` ## Per-platform content overrides ```python { "content": "Default text for all platforms", "account_ids": [2280, 2281, 2282], "platform_content": { "instagram": { "content": "Instagram-specific caption with #hashtags", "contentType": "Reels" }, "x": {"content": "Short version for X (280 char limit)"}, "linkedin": {"content": "Professional version for LinkedIn"} } } ``` Supported platform keys: `instagram`, `tiktok`, `youtube`, `linkedin`, `x`, `facebook`, `threads`, `pinterest`, `bluesky`, `telegram`, `discord`. ## Common LLM mistakes to avoid These are the patterns we see most often when LLM agents call the API incorrectly. Check your agent's behaviour against this list: 1. **Using `scheduled_at` instead of `scheduled_for`.** `scheduled_for` is canonical. `scheduled_at` still works as a deprecated alias, but your agent should always use `scheduled_for`. 2. **Calling `/complete` without actually uploading the file first.** The `/complete` endpoint verifies the file exists at the upload URL. If you skip Step 2 (the actual file upload), `/complete` returns a 400 error. Always upload bytes THEN call `/complete`. 3. **Not checking media status before creating a post.** `media_ids` in a create-post request must reference media that exists and has `status: "ready"`. If you pass a `media_id` that hasn't been completed (or failed to upload), the post creation will fail. Use `GET /v1/media/{id}` to verify status if unsure. 4. **Ignoring `validation_warnings` in 201 responses.** A post can be created successfully (201) but still include `validation_warnings`: for example, aspect ratio mismatches that will cause cropping or rejection on specific platforms. Always log or surface these warnings. 5. **Sending local timestamps without converting to UTC.** `scheduled_for` must be UTC with a `Z` suffix. Sending `"2026-04-15T09:00:00"` (no timezone) or `"2026-04-15T09:00:00-04:00"` (offset) will cause unexpected scheduling times. ## Error handling The API uses standard HTTP status codes. Error responses always have the shape: ```json { "data": null, "error": { "message": "Human-readable description", "code": "machine_readable_code", "details": { ... } }, "meta": { "request_id": "...", "timestamp": "..." } } ``` ### Retry behaviour **The simplest correct rule: only retry when `error.retryable === true`.** Every error response includes this boolean and it tells you exactly what to do. Do NOT infer retry-ability from the status code; use the field. If you must use status codes, the mapping is: - **429** (rate limited): `retryable: true`. Wait for the `Retry-After` header value (in seconds), then retry. - **500/502/503** (server error): `retryable: true`. Retry with exponential backoff (1s, 2s, 4s, 8s). Cap at ~5 attempts. - **422** (circuit breaker): `retryable: false`. The same exact request body has failed 5+ times; we will not accept it again until you **modify the request body** (or wait 6 hours). Inspect the prior 4xx responses for `validation_errors`: those tell you what to fix. - **400** (validation): `retryable: false`. Read `error.message`, `error.code`, and `error.details.validation_errors` to fix the request. - **401** (auth): `retryable: false`. The API key is invalid, expired, or revoked. - **404** (not found): `retryable: false`. The resource doesn't exist. ### CRITICAL: Do not retry the same payload after a 4xx If a 4xx response says the request is invalid (over character limit, missing field, unsupported media type, etc.), retrying the same payload will produce the same error. Always either fix the payload or stop. This is what `error.retryable: false` is telling you. We will circuit-break runaway loops automatically: after 5 identical failures from the same API key, we return **422 `permanent_failure_circuit_breaker`** instead of forwarding the request, and refuse all further attempts of that exact body for 6 hours. Do not hit this: it indicates broken retry logic on your side. ### When you get a 400 with `code: "invalid_field_name"` The error response includes `details.field_mistakes: [{wrong, right, hint}]`. Read it. Fix the wrong field name and retry once. ## Checking post results After creating a post, monitor publishing status: ```python results = requests.get( f"https://app.posteverywhere.ai/api/v1/posts/{post_id}/results", headers={...}, ).json() for dest in results["data"]["results"]: print(f"{dest['platform']} ({dest['account_name']}): {dest['status']}") if dest["status"] == "done": print(f" Live at: {dest['platform_post_url']}") elif dest["status"] == "failed": print(f" Error: {dest['error']}") ``` Status values per destination: `queued` → in flight (`preparing`, `uploading`, `publishing`, `verifying`, `retry_scheduled`) → `done` (success) or `failed` (terminal). ```` ## Adapting the prompt to your own workflow The template above is a starting point. You'll want to: 1. **Replace `pe_live_...`** with a placeholder reference like `{{POSTEVERYWHERE_API_KEY}}` so your agent knows to read the key from its environment, not hard-code it. 2. **Add your specific business context**: what kind of posts the agent should write, what platforms it has access to, what tone, what hashtag strategy. 3. **Add safety rails**: e.g. "always preview a post before scheduling", "never post to platforms outside this list", "never schedule more than 3 posts per day per account". 4. **Add your own examples**: if your agent does specific things repeatedly (e.g. weekly newsletter announcements), give it example payloads it can pattern-match. ## Related agent and API pages - [Building AI Agents](/docs/integrations/agents): Framework-agnostic patterns (Python, LangChain, OpenClaw, CrewAI) - [Using PostEverywhere with Claude](/docs/integrations/claude): Anthropic-specific setup (Claude Code, Claude Desktop, Anthropic API) - [Create Post API](/docs/api/create-post): Full endpoint reference - [Error Handling](/docs/errors): Complete error code reference and retry strategies - [Rate Limits](/docs/rate-limits): Per-minute, per-hour, per-day limits **Related:** [the quick start](/docs/quick-start) · [the AI agent overview](/agents)