Agency quickstart (Company API and MCP)

View as Markdown

Agencies manage client posts through the Company API and the company_* MCP tools. Each client is a Workspace with its own connected channels. Every call runs with the signed-in member's own role, and Publora checks permissions in each Workspace. The personal API (/create-post, MCP create_post) is a separate surface: never retry a refused Company command through it.

This page walks through one post end to end: context → channels → draft → media → client review → schedule → result and recovery.

Access

REST MCP
Endpoint https://api.publora.com/api/v1/company https://mcp.publora.com/mcp
Authentication x-publora-key: <PUBLORA_API_KEY> Authorization: Bearer <PUBLORA_API_KEY> or the member's Publora login
Client Workspace X-Publora-Workspace-Id header on post, channel, media and review calls workspaceId argument
Retry key Idempotency-Key header idempotencyKey argument
Draft fields JSON request body inside the draft argument
Channel key build platform + "-" + platformId from each channel returned as channelKey
Review reads send the approval Accept header (step 5) sent by the tool
Validation details add ?includeValidationDetails=true to schedule calls always requested

The key is the member's own Publora API key. A personal key never grants Owner authority. MCP clients read current tool schemas from tools/list; the MCP tools reference lists all Company tools.

1. Pick the Agency and the client Workspace

curl https://api.publora.com/api/v1/company/contexts/v2 \
  -H "x-publora-key: $PUBLORA_API_KEY"

MCP: company_list_contexts.

data.accounts[] lists each Company the member belongs to, with id, kind, accountRole, capabilities and workspaces[]. Each Workspace has id, name, roleSlug and capabilities such as canWriteDrafts, canUploadMedia and canScheduleOrPublish. data.defaultContext is set only when exactly one Account/Workspace pair exists.

Confirm with the user which client they mean. Never guess a Workspace or reuse one from another conversation. Check the capability before each step instead of retrying a refused command.

2. List the client's channels

curl https://api.publora.com/api/v1/company/connections \
  -H "x-publora-key: $PUBLORA_API_KEY" \
  -H "X-Publora-Workspace-Id: 64b000000000000000000201"

MCP: company_list_channels with workspaceId.

Channels are in data.items[] ([] when none are connected), each with id, platform, platformId, username, displayName and status. A channel key is platform + "-" + platformId, for example linkedin-abc123; MCP adds it as channelKey. Copy it exactly. A channel with status: "reconnect_required" cannot publish until the client reconnects it. GET /platforms (MCP company_get_platforms) returns the formats and settings Company posts support.

3. Create the draft

curl -X POST https://api.publora.com/api/v1/company/post-groups \
  -H "x-publora-key: $PUBLORA_API_KEY" \
  -H "X-Publora-Workspace-Id: 64b000000000000000000201" \
  -H "Idempotency-Key: acme-launch-draft-1" \
  -H "Content-Type: application/json" \
  -d '{"content": "Our new product is live.", "platforms": ["linkedin-abc123"]}'

The response is 201 with data.groupId, the post group ID used in every later step. A draft takes no scheduledTime or status; platforms: [] is a valid channel-less draft. Edit it with PATCH /post-groups/{postGroupId}.

MCP company_create_post wraps the same fields in draft:

{
  "workspaceId": "64b000000000000000000201",
  "idempotencyKey": "acme-launch-draft-1",
  "draft": { "content": "Our new product is live.", "platforms": ["linkedin-abc123"] }
}

Edit with company_update_post (workspaceId, postGroupId, idempotencyKey, draft). Platform options go in platformSettings (Agency platform settings); one first comment goes in firstComment (Agency first comment).

4. Attach media

Instagram, TikTok and YouTube need media before scheduling.

  1. Reserve: POST /post-groups/{postGroupId}/media/reservations with an Idempotency-Key and {"fileName", "contentType", "sizeBytes"} (MCP company_reserve_media). The response has mediaFileId, uploadUrl, uploadHeaders and expiresAt.
  2. Upload the exact bytes with HTTP PUT to uploadUrl, sending all uploadHeaders and no Publora key.
  3. Complete: POST /post-groups/{postGroupId}/media/{mediaFileId}/complete with {} and no idempotency key (MCP company_complete_media).

409 AGENCY_CORE_MEDIA_UPLOAD_PENDING means the upload is still being inspected: retry complete after the Retry-After header, do not reserve again. After AGENCY_CORE_MEDIA_RESERVATION_EXPIRED, delete the reservation and reserve again.

5. Client review (optional)

Request a review when the client approves posts before publishing:

curl -X POST https://api.publora.com/api/v1/company/post-groups/$POST_GROUP_ID/approval \
  -H "x-publora-key: $PUBLORA_API_KEY" \
  -H "X-Publora-Workspace-Id: 64b000000000000000000201" \
  -H "Idempotency-Key: acme-launch-review-1" \
  -H "Content-Type: application/json" \
  -d '{"managerComment": "Launch post for Thursday", "scheduledTime": "2026-11-20T09:00:00.000Z"}'

MCP: company_request_post_approval.

  • The response contains data.reviewUrl, a private review page valid until data.expiresAt (30 days). Publora does not send it to the client: deliver it yourself, treat it as a bearer credential and keep it out of logs.
  • When the client decides, Publora emails the user who requested the review. If notificationStatus is failed, retry with POST .../approval/notification (MCP company_retry_approval_notification).
  • Save all edits and complete media first. The command needs canScheduleOrPublish; both fields are optional.
  • With a future scheduledTime (UTC, with milliseconds), the post is scheduled automatically once approved: do not schedule it again. If that time passes before the decision, the post stays a draft and needs a new time.
  • Without scheduledTime, the approved post stays a draft until you schedule it.
  • While a review is requested, the post can be scheduled only with the client's current approval. Changing text, channels, media or settings invalidates it; request a new round. DELETE .../approval (MCP company_cancel_post_approval) removes the requirement; use it only when the user asks.
  • REST reads of the review (GET .../approval, or post reads with includeApproval=true) need Accept: application/vnd.publora.company-approval.v2+json, application/json. Without it, GET .../approval returns 409 COMPANY_APPROVAL_CLIENT_UPDATE_REQUIRED for declined and timed reviews, and post reads leave out a declined review. MCP sends the header itself.

6. Schedule

curl -X POST "https://api.publora.com/api/v1/company/post-groups/$POST_GROUP_ID/schedule?includeValidationDetails=true" \
  -H "x-publora-key: $PUBLORA_API_KEY" \
  -H "X-Publora-Workspace-Id: 64b000000000000000000201" \
  -H "Idempotency-Key: acme-launch-schedule-1" \
  -H "Content-Type: application/json" \
  -d '{"scheduledTime": "2026-11-20T09:00:00.000Z"}'

MCP: company_schedule_post. The body is exactly {"scheduledTime"} in UTC. To publish now, schedule at the current UTC time and keep that same time on retries. Confirm text, channels, client and time with the user first. Change the time with /reschedule or return the post to a draft with /unschedule (MCP company_reschedule_post, company_unschedule_post).

7. Check the result and recover failures

Scheduling only queues the post; delivery is asynchronous. GET /post-groups/{postGroupId} (MCP company_get_post) returns the group in data.group and each target in data.posts[], each with its status.

For failed targets, POST /post-groups/{postGroupId}/recover with {} and an Idempotency-Key (MCP company_recover_post) creates a new draft, returned as data.groupId, with only the failed targets and their media. Published parts are never sent again. Inspect the new draft before scheduling it.

Retries

These commands need an idempotency key: draft create, update and delete; schedule, reschedule, unschedule and recover; media reservation; requesting or cancelling a client review; channel disconnect; Workspace create, rename and archive; ownership transfer; webhook changes. Completing or deleting media, retrying a review notification and connection-link commands take no key.

After a lost response, retry with the same key, body and time, then read the resource. 409 AGENCY_CORE_IDEMPOTENCY_IN_FLIGHT means the first request is still running: wait for Retry-After and retry unchanged. 409 AGENCY_CORE_IDEMPOTENCY_KEY_CONFLICT means the key was used for a different request. A new intended operation gets a new key.

Errors

Company commands return {"success": false, "code", "message"}; API-key authentication failures return {"error"}. 402 COMPANY_BILLING_REQUIRED means the Agency's billing does not allow the command: tell the user instead of retrying. See Error handling for HTTP status handling.

Corporate accounts

A Corporate account (kind: "team", presentationMode: "people") has workspaces: []: it has no client Workspaces, channels or reviews in the Company API. Corporate employees publish their own posts through the personal API or MCP tools.