How do I plan posts on a content calendar from code or an AI agent?

Plan posts on a content calendar from code or an AI agent with the calendar REST routes or MCP tools: social:read to list, social:publish to write.

Last updated View as MarkdownAgent setup

You plan posts on a content calendar by creating calendar entries: from code, send POST /api/v1/calendar with an API key that has the social:publish scope; from an AI agent, use the create_calendar_entry MCP tool. REST and MCP use the same server functions, so each step below gives both the route and the tool.

A plan is not a post. Creating an entry creates no social post, and a plan does not schedule a post by itself. The Content Calendar page combines plans with real social posts, and browser sessions use the same routes as your code.

The steps below take you from the right key to a planned post you can see in a week's list. If the API is new to you, start with the API overview. To work from an agent, first see how to use Wireflow from your agent.

Which key scope do I need to read or edit a calendar?

You need social:read to read calendars and their entries, and social:publish to change them; social:publish also covers calendar reads on REST and MCP. Default API keys have neither permission, and a key without the scope a route needs gets 401, like every other endpoint. For keys, see API authentication.

Scope What it allows on calendars
social:read List calendars and list entries
social:publish List calendars and entries, create and edit calendars, and create, edit, move and delete entries

Your team role counts too. Team owners, admins and members can edit; viewers can read. Every request checks membership server-side, and the calendar list returns your edit access as canEdit. A team calendar can use accounts connected by any current member, but that does not grant permission to publish as another member.

For an agent, both scopes are opt-in permissions on the MCP connector at https://www.wireflow.ai/api/mcp, outside its default grant. Your client can request them by name in its OAuth scope parameter, or you can select them on the consent screen. Existing connections do not gain optional permissions automatically.

Grant the narrower scope when it is enough. Over MCP, social:publish also lets an agent post to your connected accounts, publicly under your name, while social:read cannot post. The calendar tools themselves spend no credits.

How do I find or create a calendar?

List your calendars with GET /api/v1/calendars (MCP: list_calendars), and create one with POST /api/v1/calendars (MCP: create_calendar) if none fits. A personal workspace or a team can own several calendars, one per client or brand.

curl https://www.wireflow.ai/api/v1/calendars \
  -H "Authorization: Bearer YOUR_API_KEY"

Without teamId, you get the active workspace. Pass teamId=null in the query, or "teamId": null in a JSON body, for personal calendars, or name a team you belong to. The result is { data: { calendars, accounts, members, teamId, canEdit } }. Each calendar includes its id, version, name, color, timezone, accountIds and briefUrls, and accounts never include credentials. A calendar's id is the calendarId you send in the entry calls.

To create a calendar, send its settings and an idempotencyKey:

curl -X POST https://www.wireflow.ai/api/v1/calendars \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Client launch",
    "color": "#8B5CF6",
    "timezone": "America/Los_Angeles",
    "accountIds": ["native:cm1yt4k2"],
    "briefUrls": ["https://www.wireflow.ai/flow/brand-board-id"],
    "idempotencyKey": "client-launch-calendar-v1"
  }'
Field Required What to send
name Yes The calendar's name
color Yes A six-digit hex color
timezone Yes An IANA timezone
idempotencyKey Yes A key of 8 to 200 characters
accountIds No Accounts that belong to the selected workspace; defaults to an empty list
briefUrls No https Wireflow /flow/ board links; defaults to an empty list

Social Accounts lists the accounts connected to your login, each with an id that starts with native:.

To change settings later, send PATCH /api/v1/calendars/:id (MCP: update_calendar) with expectedVersion, idempotencyKey and the settings to change. Account or board-link arrays you send replace the saved list, so send the full list you want to keep.

How do I list what is planned for a week?

Send GET /api/v1/calendar (MCP: list_calendar_entries) with from at local midnight on the first day and to at local midnight seven days later, both as ISO timestamps with timezone offsets. The range includes from and excludes to, up to 93 days. Give each midnight its own offset, because daylight saving days can be 23 or 25 hours.

curl "https://www.wireflow.ai/api/v1/calendar?calendarId=your-calendar-id&from=2026-10-18T00:00:00-07:00&to=2026-10-25T00:00:00-07:00" \
  -H "Authorization: Bearer YOUR_API_KEY"
const params = new URLSearchParams({
  calendarId: "your-calendar-id",
  from: "2026-10-18T00:00:00-07:00",
  to: "2026-10-25T00:00:00-07:00",
});
const res = await fetch(`https://www.wireflow.ai/api/v1/calendar?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
const { data } = await res.json();
for (const entry of data.entries) {
  console.log(entry.id, entry.version);
}
import requests

res = requests.get(
    "https://www.wireflow.ai/api/v1/calendar",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={
        "calendarId": "your-calendar-id",
        "from": "2026-10-18T00:00:00-07:00",
        "to": "2026-10-25T00:00:00-07:00",
    },
)
for entry in res.json()["data"]["entries"]:
    print(entry["id"], entry["version"])

The result is { data: { entries, calendars, accounts, members, teamId, canEdit } }, and it lists real posts as well as plans. Entries include id, calendarId, version, timestamp, title, platform, account, status, caption, media and links. Their activity lists who changed which fields, the time, and the API key id or MCP client name when an agent made the change.

Leave out calendarId to cover all calendars in the selected workspace. The filters status, platform, ownerId and accountId combine, as in this request that filters by platform and status:

GET /api/v1/calendar?calendarId=your-calendar-id&from=2026-10-18T00:00:00-07:00&to=2026-10-25T00:00:00-07:00&platform=instagram&status=scheduled

A plan's status is idea (the default), in_production, ready, scheduled or published. A linked post reports its real delivery status, including failed, publishing, published_private, sent_to_inbox and cancelled where applicable. sent_to_inbox means the creator must finish in the social app; it is not published.

How do I add, edit or move a planned post?

Add a plan with POST /api/v1/calendar (MCP: create_calendar_entry), edit it with PATCH /api/v1/calendar/:id (MCP: update_calendar_entry), and move it with POST /api/v1/calendar/:id/move (MCP: move_calendar_entry).

Add the plan. Send the calendar, a title, a platform, the planned time and an idempotencyKey:

curl -X POST https://www.wireflow.ai/api/v1/calendar \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "calendarId": "your-calendar-id",
    "title": "Behind the scenes",
    "platform": "instagram",
    "plannedAt": "2026-10-20T09:00:00-07:00",
    "briefUrl": "https://www.wireflow.ai/flow/your-board-id",
    "notes": "Film the new setup",
    "status": "in_production",
    "idempotencyKey": "behind-scenes-plan-v1"
  }'
const res = await fetch("https://www.wireflow.ai/api/v1/calendar", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    calendarId: "your-calendar-id",
    title: "Behind the scenes",
    platform: "instagram",
    plannedAt: "2026-10-20T09:00:00-07:00",
    briefUrl: "https://www.wireflow.ai/flow/your-board-id",
    notes: "Film the new setup",
    status: "in_production",
    idempotencyKey: "behind-scenes-plan-v1",
  }),
});
console.log(res.status, await res.json());
import requests

res = requests.post(
    "https://www.wireflow.ai/api/v1/calendar",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "calendarId": "your-calendar-id",
        "title": "Behind the scenes",
        "platform": "instagram",
        "plannedAt": "2026-10-20T09:00:00-07:00",
        "briefUrl": "https://www.wireflow.ai/flow/your-board-id",
        "notes": "Film the new setup",
        "status": "in_production",
        "idempotencyKey": "behind-scenes-plan-v1",
    },
)
print(res.status_code, res.json())

calendarId, title, platform, plannedAt and idempotencyKey are required. The optional fields are accountId, briefUrl, notes, ownerId, status and postId. The owner defaults to you and must belong to the workspace, and an account must be linked to the calendar. postId attaches an existing real post returned by publish_post, and its account and platform must match the entry.

Get its id and version. Run the week's list again. The plan is in entries with its id and version. Edits, moves and deletes need the expectedVersion returned by the latest list.

Edit it. PATCH changes only the fields you send. Send expectedVersion and idempotencyKey with them; here 3 stands for the version from your latest list:

curl -X PATCH https://www.wireflow.ai/api/v1/calendar/your-entry-id \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "ready", "expectedVersion": 3, "idempotencyKey": "behind-scenes-ready-v1"}'

Sending null clears briefUrl or notes, and accountId on an unlinked plan. Once a real post is linked, postId and the delivery fields stay locked unless the post failed with confirmation that nothing was posted and you explicitly clear or replace postId. Otherwise, sending postId: null on a linked entry returns 409 linked_post_read_only.

Move it. List again for the current version, then send the destination calendarId to the move route, adding plannedAt if the time changes too:

curl -X POST https://www.wireflow.ai/api/v1/calendar/your-entry-id/move \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"calendarId": "destination-id", "expectedVersion": 3, "idempotencyKey": "move-behind-scenes-v1"}'

You need edit access to both calendars, and the account and linked post must fit the destination. Moving a plan does not change a real post's delivery time.

Check it worked. List the week again with the calendarId the plan now sits in. Your plan is in entries, and its activity lists who changed which fields.

From an agent, ask for the same steps in plain words. The matching tools are list_calendars, create_calendar_entry and list_calendar_entries:

In my Wireflow content calendars, find the "Client launch" calendar. Plan an entry there titled "Behind the scenes" for 20 October 2026 at 09:00 Los Angeles time, platform instagram, status in_production, with the note "Film the new setup". Only plan it: do not publish anything. Then list that calendar from 18 to 25 October 2026 so I can check the entry is there.

When a plan is ready to go out, posting is a separate step: a Social Publish node or the MCP publish_post tool. Given plannedEntryId and expectedVersion, publish_post publishes to one matching account and links the queued post to the plan in the same transaction, and a version conflict posts nothing. For posting itself, see the publishing guide.

What happens if a request times out or two people edit the same entry?

If a request times out, send the identical request again with the same idempotencyKey and you get the original result. If someone else changed the entry since your last list, your write gets 409 version_conflict and changes nothing.

Every write requires an idempotencyKey of 8 to 200 characters in the JSON body. The Idempotency-Key header is not the calendar contract, so keep the key in the body. Unknown fields are refused rather than ignored.

Response Why you get it What to do
409 idempotency_conflict You reused a key with different content Reuse a key only with the identical request, and give a changed request a new key
409 version_conflict Someone else changed the row after your last list, so nothing changed List again, reconcile the new values, then send the current version and a new idempotencyKey

The version check covers entry updates, moves and deletes, and calendar updates. To see who made the other change, read the entry's activity in the list. The MCP tools follow the same contract: each write tool takes an idempotencyKey, and the edit, move and delete tools also take expectedVersion.

This Python version of the add request retries after a timeout with the same body and key, so a retry gets the original result:

import requests

body = {
    "calendarId": "your-calendar-id",
    "title": "Behind the scenes",
    "platform": "instagram",
    "plannedAt": "2026-10-20T09:00:00-07:00",
    "briefUrl": "https://www.wireflow.ai/flow/your-board-id",
    "notes": "Film the new setup",
    "status": "in_production",
    "idempotencyKey": "behind-scenes-plan-v1",
}

for _ in range(3):
    try:
        res = requests.post(
            "https://www.wireflow.ai/api/v1/calendar",
            headers={"Authorization": "Bearer YOUR_API_KEY"},
            json=body,
            timeout=30,
        )
    except requests.exceptions.Timeout:
        continue
    print(res.status_code, res.json())
    break

Does deleting a plan cancel the real post?

No: deleting a plan with DELETE /api/v1/calendar/:id (MCP: delete_calendar_entry) removes it from the calendar, and its linked real post stays scheduled or published.

DELETE takes a JSON body with expectedVersion and idempotencyKey, and there is no undo through the API:

curl -X DELETE https://www.wireflow.ai/api/v1/calendar/your-entry-id \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expectedVersion": 3, "idempotencyKey": "delete-behind-scenes-v1"}'

The other entry writes leave the real post alone too: an update never edits delivery, and moving a plan does not change a real post's delivery time.

To stop a scheduled post as well, an agent with social:publish can call the MCP cancel_post tool. It stops a scheduled post before it goes out, and once delivery has started it reports too late, never cancelled. To check the post afterwards, get_post_status reports whether posts made with publish_post are scheduled, published (with the live URL) or failed.

Next steps

  • Post to Instagram

    Post Reels, images and carousels to an Instagram Business or Creator account from a workflow, with Trial Reels, covers and collaborators.

Related

  • Social accounts API

    List the social accounts connected to your login, so a script or an agent can point a Social Publish node at them without opening the app.

  • Overview

    Share a workflow as an app with its own address and form, as an API endpoint other developers call from code, or as a social post on Instagram.

For AI agents: the full documentation index is at /llms.txt, and most docs pages are available as Markdown by adding .md to the URL.

© 2026 Wireflow. All rights reserved.