Agency quickstart (Company API and MCP)
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.
- Reserve:
POST /post-groups/{postGroupId}/media/reservationswith anIdempotency-Keyand{"fileName", "contentType", "sizeBytes"}(MCPcompany_reserve_media). The response hasmediaFileId,uploadUrl,uploadHeadersandexpiresAt. - Upload the exact bytes with HTTP
PUTtouploadUrl, sending alluploadHeadersand no Publora key. - Complete:
POST /post-groups/{postGroupId}/media/{mediaFileId}/completewith{}and no idempotency key (MCPcompany_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 untildata.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
notificationStatusisfailed, retry withPOST .../approval/notification(MCPcompany_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(MCPcompany_cancel_post_approval) removes the requirement; use it only when the user asks. - REST reads of the review (
GET .../approval, or post reads withincludeApproval=true) needAccept: application/vnd.publora.company-approval.v2+json, application/json. Without it,GET .../approvalreturns409 COMPANY_APPROVAL_CLIENT_UPDATE_REQUIREDfor 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.
Related guides
- Agency platform settings: formats, media rules and per-platform options
- Agency first comment
- Agency LinkedIn Page reshares
- Agency LinkedIn Page reads
- Agency webhooks
- MCP tools reference