This skill provides complete documentation for the Publora social media scheduling API.
When to Use This Skill
- Scheduling posts to social media platforms
- Creating, updating, or deleting social media posts
- Uploading media (images/videos) for posts
- Retrieving LinkedIn analytics (impressions, reactions, followers)
- Adding reactions and comments to LinkedIn posts
- Setting up webhooks for post status notifications
- Managing workspace team members
- Cross-platform posting workflows
API Overview
Base URL: https://api.publora.com/api/v1
Prerequisites (User Must Complete)
Important: You cannot programmatically create accounts or obtain API keys. The user must:
- Create account at publora.com (free tier available)
- Connect social accounts via OAuth in the dashboard (Channels → Add Channel)
- Generate API key at API in sidebar, then provide it to you
Pricing
| Plan | Monthly | Yearly −33% | Posts/Month | Platforms |
|---|---|---|---|---|
| Starter | Free | Free | 15 | All platforms, except X |
| Pro, per account | 1–5 · $5.996–20 · $2.9921+ · $0.99 | $3.99$1.99$0.66 | Unlimited | All platforms |
| Agency | Custom | Custom | Unlimited | All platforms |
Pro is priced per connected account and the tiers combine automatically — e.g. 10 accounts = 5 × $5.99 + 5 × $2.99 = $44.90/mo ($29.90/mo billed yearly).
Authentication
Publora uses API keys (not OAuth tokens). Keys never expire and don't require refresh.
Header: x-publora-key: sk_YOUR_API_KEY
const response = await fetch('https://api.publora.com/api/v1/platform-connections', {
headers: { 'x-publora-key': 'sk_YOUR_API_KEY' }
});
Note: The MCP server uses Authorization: Bearer sk_... instead. Same key, different header format.
Quick Reference
Agency client posts
Use Company context and exact Workspace IDs for Agency work. The
Agency platform settings guide covers
Stories, reply controls, reposts, disclosure and YouTube metadata/thumbnails.
Check the current formats and settings through GET /company/platforms.
| Endpoint | Method | Description |
|---|---|---|
/company/platforms |
GET | Current Company publishing formats and settings |
/company/post-groups |
POST | Create an Agency draft with registered platform settings |
/company/post-groups/:postGroupId |
PATCH | Update a Company draft (settings replace the map) |
/company/post-groups/:postGroupId/media/reservations |
POST | Reserve a post attachment or auxiliary youtube_thumbnail |
/company/post-groups/:postGroupId/media/:mediaFileId/complete |
POST | Complete the same Company upload reservation |
Posts
| Endpoint | Method | Description |
|---|---|---|
/list-posts |
GET | List posts with filters |
/create-post |
POST | Create a draft, schedule a post, or publish now (publishNow: true) |
/get-post/:postGroupId |
GET | Get post details |
/update-post/:postGroupId |
PUT | Edit content, targets, timing, or status of a draft/scheduled post |
/delete-post/:postGroupId |
DELETE | Delete a post |
/get-upload-url |
POST | Get presigned URL for media |
/attach-media/:postGroupId |
POST | Attach one file by reference (download_url + file_id, e.g. from ChatGPT); leaves the post in draft |
/upload-instagram-cover |
POST | Upload a custom Instagram Reel cover (multipart) |
/upload-youtube-thumbnail |
POST | Upload a custom YouTube thumbnail (multipart) |
/platform-limits |
GET | Get live per-platform limits (no params) |
/account-context |
GET | Plan, features, publishing quotas and schedule horizon (optional scheduledTime query) |
/post-logs/:postGroupId |
GET | Get publishing logs for a post |
Connections
| Endpoint | Method | Description |
|---|---|---|
/platform-connections |
GET | List connected accounts |
/test-connection/:platformId |
POST | Test if a connection is valid |
Mastodon & Bluesky Analytics
| Endpoint | Method | Description |
|---|---|---|
/post-statistics |
POST | Get engagement for up to 50 published posts |
/profile-statistics |
POST | Get followers/following/post count for a connection |
LinkedIn Analytics & Engagement
| Endpoint | Method | Description |
|---|---|---|
/linkedin-post-statistics |
POST | Get post metrics |
/linkedin-account-statistics |
POST | Get account metrics |
/linkedin-followers |
POST | Get follower count/growth |
/linkedin-profile-summary |
POST | Get profile overview |
/linkedin-reactions |
POST | Add reaction to a post |
/linkedin-reactions |
DELETE | Remove reaction from a post |
/linkedin-comments |
POST | Post a comment |
/linkedin-comments |
DELETE | Delete a comment |
/linkedin-reshare |
POST | Reshare an existing post |
/linkedin-mentionables |
GET | List mentionable people (paid plans) |
Webhooks
| Endpoint | Method | Description |
|---|---|---|
/webhooks |
GET | List all webhooks |
/webhooks |
POST | Create a webhook |
/webhooks/:id |
PATCH | Update a webhook |
/webhooks/:id |
DELETE | Delete a webhook |
/webhooks/:id/regenerate-secret |
POST | Regenerate webhook secret |
Workspace
Note: Workspace access requires approval. Contact support@publora.com to enable.
| Endpoint | Method | Description |
|---|---|---|
/workspace/users |
GET | List team members |
/workspace/users |
POST | Add team member |
/workspace/users/attach |
POST | Attach an existing Publora user (needs the user's consent token) |
/workspace/users/:userId |
DELETE | Remove team member |
Common Patterns
Schedule a Post
const response = await fetch('https://api.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'sk_YOUR_API_KEY'
},
body: JSON.stringify({
content: 'Hello world!',
platforms: ['linkedin-ABC123', 'twitter-456'],
scheduledTime: '2026-03-01T14:00:00.000Z'
})
});
Publish Now
Send publishNow: true instead of scheduledTime to publish immediately at server time. It uses the monthly post allowance but no scheduled-queue slot; a future scheduledTime or status: "draft" alongside it returns 400 PUBLISH_NOW_CONFLICT. PUT /update-post/:postGroupId with { "publishNow": true } publishes an existing draft or scheduled post.
await fetch('https://api.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'sk_YOUR_API_KEY',
'Idempotency-Key': 'launch-2026-03-01'
},
body: JSON.stringify({
content: 'Hello world!',
platforms: ['linkedin-ABC123'],
publishNow: true
})
});
Check Limits Before a Batch
GET /account-context returns the plan, features, monthly and queue quotas (limit: null = unlimited), maxScheduledDate and allowedPlatforms. It is an advisory snapshot; writes are still checked.
Get Connected Platforms
const response = await fetch('https://api.publora.com/api/v1/platform-connections', {
headers: { 'x-publora-key': 'sk_YOUR_API_KEY' }
});
const { connections } = await response.json();
// Use connections[].platformId for posting
Get LinkedIn Analytics
const response = await fetch('https://api.publora.com/api/v1/linkedin-post-statistics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'sk_YOUR_API_KEY'
},
body: JSON.stringify({
postedId: 'urn:li:share:7123456789',
platformId: 'linkedin-ABC123',
queryTypes: 'ALL'
})
});
Comment on LinkedIn Post
const response = await fetch('https://api.publora.com/api/v1/linkedin-comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'sk_YOUR_API_KEY'
},
body: JSON.stringify({
postedId: 'urn:li:ugcPost:7123456789',
platformId: 'linkedin-ABC123',
message: 'Great insights! Thanks for sharing.'
})
});
Create Webhook
const response = await fetch('https://api.publora.com/api/v1/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'sk_YOUR_API_KEY'
},
body: JSON.stringify({
name: 'Production Notifications',
url: 'https://your-server.com/webhook',
events: ['post.published', 'post.failed']
})
});
Key Concepts
Platform IDs
Platform IDs follow the format platform-id:
twitter-123456789linkedin-Tz9W5i6ZYGinstagram-17841412345678
Get platform IDs from the /platform-connections endpoint.
Scheduled Time Format
Use ISO 8601 UTC format:
2026-03-15T14:00:00.000Z // Correct
2026-03-15T14:00:00Z // Also correct
Time must be in the future. A past time is never silently accepted:
- Less than 5 min in the past (clock skew): clamped to server time, and the
200response carrieswarnings: [{ code: "SCHEDULED_TIME_COERCED", requested, effective }]. This tolerance is permanent. - 5 min or more in the past: clamped + warned today; scheduled to become
400 SCHEDULED_TIME_IN_PASTon 2026-08-25, unless production configuration overrides the date either way.
A 200 can still mean the API adjusted your request — read warnings, don't discard it. GET /get-post returns the effective stored scheduledTime to confirm what was actually kept.
Retry Safety (Idempotency-Key)
Send an optional Idempotency-Key header on create-post / update-post / attach-media. A timeout never tells you whether the request landed — without a key, the retry creates a second post (and on update-post with mediaUrls, or on attach-media, appends the media twice). On attach-media the key matches the post, file_id and file name, so a refreshed download_url still replays.
const key = crypto.randomUUID(); // one key per logical operation — reuse it for every retry
await fetch('https://api.publora.com/api/v1/create-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-publora-key': 'sk_YOUR_API_KEY',
'Idempotency-Key': key
},
body: JSON.stringify({ content: 'Hello', platforms: ['twitter-123456789'] })
});
- Same key + same body → the original response is replayed (no second post).
- Same key + different body →
422/IDEMPOTENCY_KEY_CONFLICT. - Same key still processing →
409/IDEMPOTENCY_IN_FLIGHT— wait and retry the identical call, don't switch keys. - Keys are per-user and expire after 24h. Opt-in: without the header, behaviour is unchanged.
Post Statuses
draft- Not scheduledscheduled- Waiting to publishpublished- Successfully postedfailed- Publishing failedpartially_published- Some platforms failed
LinkedIn Query Types
Available metrics: IMPRESSION, MEMBERS_REACHED, RESHARE, REACTION, COMMENT
Use queryTypes: 'ALL' to get all metrics at once.
Webhook Events
Available events:
post.scheduled- Post was scheduledpost.published- Post was successfully publishedpost.failed- Post failed to publishpost.demoted- Scheduled post returned to draft.reasonismedia_changed(changeTypeattach,detach,prune_referenceorreorder),media_backstop(changeTypescheduler_media_backstop, witherrorCode) orstate_mismatch(changeTypescheduler_state_repair); tolerate unknown values. Not emitted byattach-mediatoken.expiring- Defined and subscribable, but not currently dispatched; do not depend on it
Error Handling
Always check response status:
if (!response.ok) {
const error = await response.json();
console.error(error.error); // Error message
}
Common errors:
- 401: Invalid API key
- 400: Invalid parameters
- 404: Resource not found
Machine-readable code values (branch on code, not on the message text):
SCHEDULED_TIME_IN_PAST(400) —scheduledTime5+ min in the past; body carriesserverTimePLATFORM_SETTING_UNKNOWN(400) — unknownplatformSettingspath; body carries the exactfieldFIRST_COMMENT_INVALID/FIRST_COMMENT_PLATFORM_UNKNOWN/FIRST_COMMENT_TOO_LONG(400) —firstCommentorfirstComments[]shape (up to three ordered entries with optional integerdelaySeconds0–86,400), platform type (never connection IDs) or per-platform length; body carriesfieldand, for length,platform,limit,count,unit. Use one input field, not both.INVALID_MEDIA_FILE(400) —attach-mediabody is not{ file, fileName? }, the file reference is malformed,download_urlfails the URL checks, or the file name is unusableIDEMPOTENCY_KEY_CONFLICT(422) — key reused with a different bodyIDEMPOTENCY_IN_FLIGHT(409) — same key still processing; retry the identical call
Successful responses may carry a warnings array (e.g. SCHEDULED_TIME_COERCED) — surface it. Full reference: Error Handling.
Resources
See the docs/ directory for:
docs/endpoints/- Detailed endpoint documentationdocs/guides/- How-to guidesdocs/examples/- Code examples in multiple languagesdocs/platforms/- Platform-specific information
For agent-facing skills built on this API, see publora/skills.
Agency Workspace webhooks
Agency managers use Company /webhooks or company_webhooks, with an explicit
Workspace and stable mutation idempotency keys. See
Agency webhooks; personal webhook endpoints
do not access Company subscriptions.