{"skill":{"slug":"publora","displayName":"Publora","summary":"Publora API — schedule and publish social media posts across 10 platforms (X/Twitter, LinkedIn, Instagram, Threads, TikTok, YouTube, Facebook, Bluesky, Masto...","description":"---\nname: publora\ndescription: >\n  Publora API — schedule and publish social media posts across 10 platforms\n  (X/Twitter, LinkedIn, Instagram, Threads, TikTok, YouTube, Facebook, Bluesky,\n  Mastodon, Telegram). Use this skill when the user wants to post,\n  schedule, draft, bulk-schedule, manage workspace users, configure webhooks,\n  or retrieve LinkedIn analytics via Publora.\n---\n\n# Publora API — Core Skill\n\nPublora is an affordable REST API for scheduling and publishing social media posts\nacross 10 platforms (Pinterest is listed internally but not yet supported). Base URL: `https://api.publora.com/api/v1`\n\n## Plans & API Access\n\n| Plan | Price | Posts/Month | Platforms |\n|------|-------|-------------|-----------|\n| Starter | Free | 15 | LinkedIn & Bluesky |\n| Pro | $2.99/account | 100/account | All |\n| Premium | $5.99/account | 500/account | All |\n\n> ℹ️ Starter gives API access for LinkedIn and Bluesky. Twitter/X requires Pro or Premium (explicitly excluded from Starter). See [publora.com/pricing](https://publora.com/pricing).\n\n## Authentication\n\nAll requests require the `x-publora-key` header. Keys start with `sk_` (format: `sk_xxxxxxx.xxxxxx...`).\n\n```bash\ncurl https://api.publora.com/api/v1/platform-connections \\\n  -H \"x-publora-key: sk_YOUR_KEY\"\n```\n\nGet your key: [publora.com](https://publora.com) → Settings → API Keys → Generate API Key.\n⚠️ Copy immediately — shown only once.\n\n## Step 0: Get Platform IDs\n\n**Always call this first** to get valid platform IDs before posting.\n\n```javascript\nconst res = await fetch('https://api.publora.com/api/v1/platform-connections', {\n  headers: { 'x-publora-key': 'sk_YOUR_KEY' }\n});\nconst { connections } = await res.json();\n// connections[i].platformId → e.g. \"linkedin-ABC123\", \"twitter-456\"\n// Also returns: tokenStatus, tokenExpiresIn, lastSuccessfulPost, lastError\n```\n\nPlatform IDs look like: `twitter-123`, `linkedin-ABC`, `instagram-456`, `threads-789`, etc.\n\n## Post Immediately\n\nOmit `scheduledTime` to publish right away:\n\n```javascript\nawait fetch('https://api.publora.com/api/v1/create-post', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' },\n  body: JSON.stringify({\n    content: 'Your post content here',\n    platforms: ['twitter-123', 'linkedin-ABC']\n  })\n});\n```\n\n## Schedule a Post\n\nInclude `scheduledTime` in ISO 8601 UTC — must be in the future:\n\n```javascript\nawait fetch('https://api.publora.com/api/v1/create-post', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' },\n  body: JSON.stringify({\n    content: 'Scheduled post content',\n    platforms: ['twitter-123', 'linkedin-ABC'],\n    scheduledTime: '2026-03-16T10:00:00.000Z'\n  })\n});\n// Response: { postGroupId: \"pg_abc123\", scheduledTime: \"...\" }\n```\n\n## Save as Draft\n\nOmit `scheduledTime` — post is created as draft. Schedule it later:\n\n```javascript\n// Create draft\nconst { postGroupId } = await createPost({ content, platforms });\n\n// Schedule later\nawait fetch(`https://api.publora.com/api/v1/update-post/${postGroupId}`, {\n  method: 'PUT',\n  headers: { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' },\n  body: JSON.stringify({ status: 'scheduled', scheduledTime: '2026-03-16T10:00:00.000Z' })\n});\n```\n\n## List Posts\n\nFilter, paginate and sort your scheduled/published posts:\n\n```javascript\n// GET /api/v1/list-posts\n// Query params: status, platform, fromDate, toDate, page, limit, sortBy, sortOrder\nconst res = await fetch(\n  'https://api.publora.com/api/v1/list-posts?status=scheduled&platform=twitter&page=1&limit=20',\n  { headers: { 'x-publora-key': 'sk_YOUR_KEY' } }\n);\nconst { posts, pagination } = await res.json();\n// pagination: { page, limit, totalItems, totalPages, hasNextPage, hasPrevPage }\n```\n\nValid statuses: `draft`, `scheduled`, `published`, `failed`, `partially_published`\n\n## Get / Delete a Post\n\n```bash\n# Get post details\nGET /api/v1/get-post/:postGroupId\n\n# Delete post (also removes media from storage)\nDELETE /api/v1/delete-post/:postGroupId\n```\n\n## Get Post Logs\n\nDebug failed or partially published posts:\n\n```javascript\nconst res = await fetch(\n  `https://api.publora.com/api/v1/post-logs/${postGroupId}`,\n  { headers: { 'x-publora-key': 'sk_YOUR_KEY' } }\n);\nconst { logs } = await res.json();\n```\n\n## Test a Connection\n\nVerify a platform connection is healthy before posting:\n\n```javascript\nconst res = await fetch(\n  'https://api.publora.com/api/v1/test-connection/linkedin-ABC123',\n  { method: 'POST', headers: { 'x-publora-key': 'sk_YOUR_KEY' } }\n);\n// Returns: { status: \"ok\"|\"error\", message, permissions, tokenExpiresIn }\n```\n\n## Bulk Schedule (a Week of Content)\n\n```python\nfrom datetime import datetime, timedelta, timezone\nimport requests\n\nHEADERS = { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' }\nbase_date = datetime(2026, 3, 16, 10, 0, 0, tzinfo=timezone.utc)\n\nposts = ['Monday post', 'Tuesday post', 'Wednesday post', 'Thursday post', 'Friday post']\n\nfor i, content in enumerate(posts):\n    scheduled_time = base_date + timedelta(days=i)\n    requests.post('https://api.publora.com/api/v1/create-post', headers=HEADERS, json={\n        'content': content,\n        'platforms': ['twitter-123', 'linkedin-ABC'],\n        'scheduledTime': scheduled_time.isoformat()\n    })\n```\n\n## Media Uploads\n\nAll media (images and videos) use a 3-step pre-signed upload workflow:\n\n**Step 1:** `POST /api/v1/create-post` → get `postGroupId`  \n**Step 2:** `POST /api/v1/get-upload-url` → get `uploadUrl`  \n**Step 3:** `PUT {uploadUrl}` with file bytes (no auth needed for S3)\n\n```python\nimport requests\n\nHEADERS = { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' }\n\n# Step 1: Create post\npost = requests.post('https://api.publora.com/api/v1/create-post', headers=HEADERS, json={\n    'content': 'Check this out!',\n    'platforms': ['instagram-456'],\n    'scheduledTime': '2026-03-15T14:30:00.000Z'\n}).json()\npost_group_id = post['postGroupId']\n\n# Step 2: Get pre-signed upload URL\nupload = requests.post('https://api.publora.com/api/v1/get-upload-url', headers=HEADERS, json={\n    'fileName': 'photo.jpg',\n    'contentType': 'image/jpeg',\n    'type': 'image',  # or 'video'\n    'postGroupId': post_group_id\n}).json()\n\n# Step 3: Upload directly to S3 (no auth header needed)\nwith open('./photo.jpg', 'rb') as f:\n    requests.put(upload['uploadUrl'], headers={'Content-Type': 'image/jpeg'}, data=f)\n```\n\nFor carousels: call `get-upload-url` N times with the **same `postGroupId`**.\n\n## Cross-Platform Threading\n\nX/Twitter and Threads support threading. Three methods:\n\n- **Auto-split**: Content over the char limit is split automatically at paragraph/sentence/word breaks. Publora adds `(1/N)` markers (e.g. `(1/3)`).\n- **Manual `---`**: Use `---` on its own line to define exact split points.\n- **Explicit `[n/m]`**: Use `[1/3]`, `[2/3]` markers — Publora preserves them as-is.\n\n```javascript\n// Manual split example\nbody: JSON.stringify({\n  content: 'First tweet.\\n\\n---\\n\\nSecond tweet.\\n\\n---\\n\\nThird tweet.',\n  platforms: ['twitter-123']\n})\n```\n\n> ⚠️ **Threads Restriction:** Multi-threaded nested posts are **temporarily unavailable on Threads** (connected replies). Single posts, images, and carousels work normally. Contact support@publora.com for updates.\n\n## LinkedIn Analytics\n\n```javascript\n// Post statistics — queryTypes is an ARRAY (not a string; 'ALL' is invalid here)\n// Use queryType (singular string) for one metric, queryTypes (array) for multiple\nawait fetch('https://api.publora.com/api/v1/linkedin-post-statistics', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' },\n  body: JSON.stringify({\n    postedId: 'urn:li:share:7123456789',\n    platformId: 'linkedin-ABC123',\n    queryTypes: ['IMPRESSION', 'MEMBERS_REACHED', 'RESHARE', 'REACTION', 'COMMENT']\n    // OR: queryType: 'IMPRESSION'  ← singular, returns { count: 123 }\n    // Multi-metric response: { metrics: { IMPRESSION: 4521, MEMBERS_REACHED: 3200, ... } }\n  })\n});\n\n// Profile summary (followers + aggregated stats)\nawait fetch('https://api.publora.com/api/v1/linkedin-profile-summary', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' },\n  body: JSON.stringify({ platformId: 'linkedin-ABC123' })\n});\n```\n\nAvailable analytics endpoints:\n\n| Endpoint | Description |\n|----------|-------------|\n| `POST /linkedin-post-statistics` | Impressions, reactions, reshares for a post |\n| `POST /linkedin-account-statistics` | Aggregated account metrics |\n| `POST /linkedin-followers` | Follower count and growth |\n| `POST /linkedin-profile-summary` | Combined profile overview |\n| `POST /linkedin-create-reaction` | React to a post |\n| `DELETE /linkedin-delete-reaction` | Remove a reaction |\n\n## Webhooks\n\nGet real-time notifications when posts are published, fail, or tokens are expiring.\n\n```javascript\n// Create a webhook\nawait fetch('https://api.publora.com/api/v1/webhooks', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-publora-key': 'sk_YOUR_KEY' },\n  body: JSON.stringify({\n    name: 'My webhook',\n    url: 'https://myapp.com/webhooks/publora',\n    events: ['post.published', 'post.failed', 'token.expiring']\n  })\n});\n// Returns: { webhook: { _id, name, url, events, secret, isActive } }\n// Save the `secret` — it's only shown once. Use it to verify webhook signatures.\n```\n\nValid events: `post.scheduled`, `post.published`, `post.failed`, `token.expiring`\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/webhooks` | GET | List all webhooks |\n| `/webhooks` | POST | Create webhook |\n| `/webhooks/:id` | PATCH | Update webhook |\n| `/webhooks/:id` | DELETE | Delete webhook |\n| `/webhooks/:id/regenerate-secret` | POST | Rotate webhook secret |\n\nMax 10 webhooks per account.\n\n## Workspace / B2B API\n\nManage multiple users under your workspace account. Contact serge@publora.com to enable Workspace API access.\n\n```javascript\n// Create a managed user (returns HTTP 201)\nconst { user } = await fetch('https://api.publora.com/api/v1/workspace/users', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-publora-key': 'sk_CORP_KEY' },\n  body: JSON.stringify({ username: 'client@example.com', displayName: 'Acme Corp' })\n}).then(r => r.json());\n// user._id is the MongoDB ObjectId (24-char hex), e.g. \"6626a1f5e4b0c91a2d3f4567\"\n\n// Generate connection URL for user to connect their social accounts\nconst { connectionUrl } = await fetch(\n  `https://api.publora.com/api/v1/workspace/users/${user._id}/connection-url`,\n  { method: 'POST', headers: { 'x-publora-key': 'sk_CORP_KEY' } }\n).then(r => r.json());\n\n// Post on behalf of managed user\nawait fetch('https://api.publora.com/api/v1/create-post', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'x-publora-key': 'sk_CORP_KEY',\n    'x-publora-user-id': user._id  // ← key header for acting on behalf of a user\n  },\n  body: JSON.stringify({ content: 'Post for Acme Corp!', platforms: ['linkedin-XYZ'] })\n});\n```\n\nWorkspace endpoints:\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/workspace/users` | GET | List managed users |\n| `/workspace/users` | POST | Create managed user |\n| `/workspace/users/:userId` | DELETE | Detach managed user (preserves user record, removes workspace association) |\n| `/workspace/users/:userId/api-key` | POST | Generate per-user API key |\n| `/workspace/users/:userId/connection-url` | POST | Generate OAuth connection link |\n\nEach managed user has a `dailyPostsLeft` field (default: 100) — note this is informational only and **not enforced as an actual posting limit**. Real limits are workspace-level: `monthlyPosts`, `scheduledPosts`, `scheduleHorizonDays` — enforced at scheduling time. Never expose your workspace key client-side — use per-user API keys for client-facing scenarios.\n\n## Platform Limits Quick Reference (API)\n\n> ⚠️ API limits are often stricter than native app limits. Always design against these.\n\n| Platform | Char Limit | Max Images | Video Max | Text Only? |\n|----------|-----------|-----------|-----------|------------|\n| Twitter/X | 280 (25K Premium) | 4 × 5MB | 2 min / 512MB | ✅ |\n| LinkedIn | 3,000 | 10 × 5MB | 30 min / 500MB | ✅ |\n| Instagram | 2,200 | **10 × 8MB (JPEG only)** | **3 min (180s)** Reels / 60s Stories / 300MB | ❌ |\n| Threads | 500 | 20 × 8MB | 5 min / 500MB | ✅ |\n| TikTok | 2,200 | Video only | 10 min / 4GB | ❌ |\n| YouTube | 5,000 desc | Video only | 12h / 256GB | ❌ |\n| Facebook | 63,206 | 10 × 10MB | 45 min / 2GB | ✅ |\n| Bluesky | 300 | 4 × 1MB | 3 min / 100MB | ✅ |\n| Mastodon | 500 | 4 × 16MB | ~99MB | ✅ |\n| Telegram | 4,096 (1,024 captions) | 10 × 10MB | 50MB (Bot API) | ✅ |\n\nFor full limits detail, see the `docs/guides/platform-limits.md` in the [Publora API Docs](https://github.com/publora/publora-api-docs).\n\n## Platform-Specific Skills\n\nFor platform-specific settings, limits, and examples:\n\n- `publora-linkedin` — LinkedIn posts + analytics + reactions\n- `publora-twitter` — X/Twitter posts & threads\n- `publora-instagram` — Instagram images/reels/carousels\n- `publora-threads` — Threads posts\n- `publora-tiktok` — TikTok videos\n- `publora-youtube` — YouTube videos\n- `publora-facebook` — Facebook page posts\n- `publora-bluesky` — Bluesky posts\n- `publora-mastodon` — Mastodon posts\n- `publora-telegram` — Telegram channels\n\n## Post Statuses\n\n- `draft` — Not scheduled yet\n- `scheduled` — Waiting to publish\n- `published` — Successfully posted\n- `failed` — Publishing failed (check `/post-logs`)\n- `partially_published` — Some platforms failed\n\n## Errors\n\n| Code | Meaning |\n|------|---------|\n| 400 | Invalid request (check `scheduledTime` format, required fields) |\n| 401 | Invalid or missing API key |\n| 403 | Plan limit reached or Workspace API not enabled |\n| 404 | Post/resource not found |\n| 429 | Platform rate limit exceeded |\n","tags":{"latest":"1.2.1"},"stats":{"comments":0,"downloads":1199,"installsAllTime":2,"installsCurrent":2,"stars":0,"versions":4},"createdAt":1771618238559,"updatedAt":1778491594064},"latestVersion":{"version":"1.2.1","createdAt":1774011608704,"changelog":"Fix: Starter plan gives API access (LinkedIn & Bluesky); Twitter requires Pro+. Fix LinkedIn analytics queryTypes array. Fix threading: nested threads disabled on Threads. Fix workspace: username, _id, dailyPostsLeft.","license":"MIT-0"},"metadata":null,"owner":{"handle":"sergebulaev","userId":"s177bqjwcnr0pvsgvpzgeayqrx84tk9r","displayName":"Sergey Bulaev","image":"https://avatars.githubusercontent.com/u/241980?v=4"},"moderation":{"isSuspicious":false,"isMalwareBlocked":false,"verdict":"clean","reasonCodes":["review.llm_review"],"summary":"Review: review.llm_review","engineVersion":"v2.4.24","updatedAt":1779943568489}}