Skill: peturgeorgievv/postfast (version 1.9.0)

--- name: postfast description: Schedule and manage social media posts across TikTok, Instagram, Facebook, X (Twitter), YouTube, LinkedIn, Threads, Bluesky, Pinterest, Telegram, and Google Business Profile using the PostFast API. Use when the user wants to schedule social media posts, manage social media content, upload media for social posting, list connected social accounts, check scheduled posts, delete scheduled posts, cross-post content to multiple platforms, manage Google Business Profile posts, or automate their social media workflow. PostFast is a SaaS tool — no self-hosting required. homepage: https://postfa.st metadata: {"openclaw":{"emoji":"⚡","primaryEnv":"POSTFAST_API_KEY","requires":{"env":["POSTFAST_API_KEY"]}}} --- # PostFast Schedule social media posts across 11 platforms from one API. SaaS — no self-hosting needed. ## Setup 1. Sign up at https://app.postfa.st/register (7-day free trial, no credit card) 2. Go to Workspace Settings → generate an API key 3. Set the environment variable: ```bash export POSTFAST_API_KEY="your-api-key" ``` Base URL: `https://api.postfa.st` Auth header: `pf-[REDACTED_SECRET]` **Important:** The header name is `pf-api-key` (not `Authorization: Bearer` or `x-api-key`). Regenerating your key in settings permanently invalidates the previous one. See [Troubleshooting](#troubleshooting) if you get 403 errors. ## Core Workflow ### 1. List connected accounts ```bash curl -s -H "pf-[REDACTED_SECRET]" https://api.postfa.st/social-media/my-social-accounts ``` Returns array of `{ id, platform, platformUsername, displayName, connectionStatus, disabledReason }`. Save the `id` — it's the `socialMediaId` required for every post. - `connectionStatus` (always present) — `CONNECTED` (healthy) or `DISABLED` (paused; the account needs reconnecting and won't publish until the user does). - `disabledReason` — `null` unless `DISABLED`, then one of `TOKEN_REVOKED`, `ACCOUNT_SUSPENDED`, `PERMISSION_REVOKED`, `MANUAL`. Check `connectionStatus` before scheduling — a `DISABLED` account rejects new scheduled posts (drafts still work). Platform values: `TIKTOK`, `INSTAGRAM`, `FACEBOOK`, `X`, `YOUTUBE`, `LINKEDIN`, `THREADS`, `BLUESKY`, `PINTEREST`, `TELEGRAM`, `GOOGLE_BUSINESS_PROFILE` ### 2. Schedule a text post (no media) ```bash curl -X POST https://api.postfa.st/social-posts \ -H "pf-[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{ "posts": [{ "content": "Your post text here", "mediaItems": [], "scheduledAt": "2026-06-15T10:00:00.000Z", "socialMediaId": "ACCOUNT_ID_HERE" }], "controls": {} }' ``` Returns `{ "postIds": ["uuid-1"] }`. ### 3. Schedule a post with media (3-step flow) **Step A** — Get signed upload URLs: ```bash curl -X POST https://api.postfa.st/file/get-signed-upload-urls \ -H "pf-[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{ "contentType": "image/png", "count": 1 }' ``` Returns `[{ "key": "image/uuid.png", "signedUrl": "https://..." }]`. **Step B** — Upload file to S3: ```bash curl -X PUT "SIGNED_URL_HERE" \ -H "Content-Type: image/png" \ --data-binary @/path/to/file.png ``` **Step C** — Create post with media key: ```bash curl -X POST https://api.postfa.st/social-posts \ -H "pf-[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{ "posts": [{ "content": "Post with image!", "mediaItems": [{ "key": "image/uuid.png", "type": "IMAGE", "sortOrder": 0 }], "scheduledAt": "2026-06-15T10:00:00.000Z", "socialMediaId": "ACCOUNT_ID_HERE" }], "controls": {} }' ``` For video: use `contentType: "video/mp4"`, `type: "VIDEO"`, key prefix `video/`. ### 4. List scheduled posts ```bash curl -s -H "pf-[REDACTED_SECRET]" "https://api.postfa.st/social-posts?page=0&limit=20" ``` Returns `{ "data": [...], "totalCount": 25, "pageInfo": { "page": 1, "hasNextPage": true, "perPage": 20 } }`. **Query parameters:** - `page` (int, default 0) — 0-based page index. Response shows 1-based display page in `pageInfo.page` - `limit` (int, default 20, max 50) — items per page - `platforms` (string) — comma-separated filter: `FACEBOOK,INSTAGRAM,X` - `statuses` (string) — comma-separated: `DRAFT`, `SCHEDULED`, `PUBLISHED`, `FAILED` - `from` / `to` (ISO 8601 UTC) — date range filter on `scheduledAt` Example: `GET /social-posts?page=0&limit=50&platforms=X,LINKEDIN&statuses=SCHEDULED&from=2026-06-01T00:00:00Z&to=2026-06-30T23:59:59Z` ### 5. Delete a scheduled post ```bash curl -X DELETE -H "pf-[REDACTED_SECRET]" https://api.postfa.st/social-posts/POST_ID ``` ### 6. Cross-post to multiple platforms Include multiple entries in the `posts` array, each with a different `socialMediaId`. They share the same `controls` and `mediaItems` keys. ### 7. Generate a connect link (for clients) Let clients connect their social accounts to your workspace without creating a PostFast account: ```bash curl -X POST https://api.postfa.st/social-media/connect-link \ -H "pf-[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{ "expiryDays": 7, "sendEmail": true, "email": "[REDACTED_SECRET]" }' ``` Returns `{ "connectUrl": "https://app.postfa.st/connect?token=..." }`. Share the URL — they can connect accounts directly. Rate limit: 50/hour. ### 8. Create a draft post **Two unrelated concepts share the word "draft" — don't mix them up:** | What you want | How | |---------------|-----| | **PostFast draft** (any platform) — saved in PostFast, not scheduled, user finalizes from the dashboard | Set `status: "DRAFT"` and **omit** `scheduledAt`. Works for every platform. | | **TikTok app draft** — pushes the post to the TikTok app's draft inbox so the user finishes editing on their phone | Set `controls.tiktokIsDraft: true`. TikTok-only. This is **not** a PostFast draft state — it still needs `scheduledAt`. | **⚠️ Common mistake — `status` placement:** `status` is a **top-level** field, sibling of `posts` and `controls`. It is **not** a per-post field. If you put it inside the post object, the API silently ignores it, defaults to `SCHEDULED`, and rejects the request with `"All posts must have scheduledAt when status is not present, as default is SCHEDULED"`. ```jsonc // ❌ Wrong — status nested inside the post { "posts": [{ "content": "...", "status": "DRAFT", "socialMediaId": "..." }] } // ✅ Right — status at top level { "posts": [{ "content": "...", "socialMediaId": "..." }], "status": "DRAFT", "controls": {} } ``` Per-post fields: `content`, `mediaItems`, `socialMediaId`, `scheduledAt` (optional for drafts), `firstComment`. Top-level fields: `status`, `approvalStatus`, `controls`. **PostFast draft (recommended for "save now, schedule later"):** ```bash curl -X POST https://api.postfa.st/social-posts \ -H "pf-[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{ "posts": [{ "content": "Draft idea...", "mediaItems": [], "socialMediaId": "ACCOUNT_ID" }], "status": "DRAFT", "controls": {} }' ``` See [examples/draft-post.json](examples/draft-post.json) for the platform-agnostic pattern, or [examples/tiktok-app-draft.json](examples/tiktok-app-draft.json) for the TikTok-app-inbox case. ### 9. Get post analytics Fetch published posts with their performance metrics: ```bash curl -s -H "pf-[REDACTED_SECRET]" \ "https://api.postfa.st/social-posts/analytics?startDate=2026-03-01T00:00:00.000Z&endDate=2026-03-31T23:59:59.999Z&platforms=TIKTOK,INSTAGRAM" ``` **Query parameters:** - `startDate` (ISO 8601, required) — start of date range - `endDate` (ISO 8601, required) — end of date range - `platforms` (string, optional) — comma-separated filter - `socialMediaIds` (string, optional) — comma-separated account UUIDs Returns `{ "data": [{ id, content, socialMediaId, platformPostId, publishedAt, latestMetric }] }`. `latestMetric` fields: `impressions`, `reach`, `likes`, `comments`, `shares`, `totalInteractions`, `fetchedAt`, `extras`. All numbers are strings (bigint). `latestMetric` is null if metrics haven't been fetched yet. **Supported platforms for analytics:** Facebook, Instagram, Threads, LinkedIn, TikTok, YouTube, Pinterest. LinkedIn personal accounts are excluded. YouTube returns views, likes, comments, and total interactions (no reach or shares). **Pinterest specifics** (requires a Pinterest business account): Canonical fields: - `impressions`, `likes` (reactions: heart/applaud/idea/etc.), `comments` — **lifetime** totals from Pinterest's pin endpoint. - `shares` — **90-day rolling save count**. This is the only canonical field that isn't lifetime; Pinterest's v5 API does not expose lifetime save totals. - `totalInteractions` — sum of pin clicks + outbound clicks + saves + reactions + comments. Mostly lifetime, but the saves portion is 90-day, so treat it as a "best available" interaction total rather than strictly lifetime. - `reach` — always null (Pinterest doesn't return it). `extras` (lifetime): - `pin_clicks` — opens of the pin's close-up view - `outbound_clicks` — clicks to the destination URL - `engagement`, `engagement_rate`, `pin_click_rate`, `outbound_click_rate` — Pinterest's lifetime analytics fields, passed through when present `extras` (90-day rolling): - `impressions_90d`, `pin_clicks_90d`, `outbound_clicks_90d` — same metrics as their lifetime counterparts but for the last 90 days - `save_rate` — saves ÷ impressions over the 90-day window `extras` for video pins (when present): - `video_mrc_views`, `video_avg_watch_time`, `video_v50_watch_time`, `video_10s_views`, `video_start`, `quartile_95_percent_view` Refresh cadence: every 6 hours for pins under 14 days old, once a day for pins 14–60 days old. You can also trigger a manual refresh from the dashboard. Rate limit: 350/hour. ## Common Patterns ### Pattern 1: Cross-platform campaign Post the same content to LinkedIn, X, and Threads at the same time: ```bash curl -X POST https://api.postfa.st/social-posts \ -H "pf-[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{ "posts": [ { "content": "Big announcement!", "mediaItems": [], "scheduledAt": "2026-06-15T09:00:00.000Z", "socialMediaId": "LINKEDIN_ID" }, { "content": "Big announcement!", "mediaItems": [], "scheduledAt": "2026-06-15T09:00:00.000Z", "socialMediaId": "X_ID" }, { "content": "Big announcement!", "mediaItems": [], "scheduledAt": "2026-06-15T09:00:00.000Z", "socialMediaId": "THREADS_ID" } ], "controls": {} }' ``` See [examples/cross-platform-post.json](examples/cross-platform-post.json) for a complete example. ### Pattern 2: Instagram Reel with upload 1. Get signed URL with `contentType: "video/mp4"` 2. PUT video to signed URL 3. Create post with `instagramPublishType: "REEL"` Optional: add a custom cover image by uploading a JPEG (max 8MB) via the same 3-step flow, then set `coverImageKey` in the media item. You can also set `coverTimestamp` (milliseconds) as a fallback frame. See [examples/instagram-reel.json](examples/instagram-reel.json) for the basic request, or [examples/instagram-reel-cover.json](examples/instagram-reel-cover.json) for a Reel with a custom cover image. ### Pattern 3: TikTok video with privacy settings Upload video, then post with privacy controls: ```bash # controls object: { "tiktokPrivacy": "PUBLIC", "tiktokAllowComments": true, "tiktokAllowDuet": false, "tiktokAllowStitch": false, "tiktokBrandContent": true } ``` See [examples/tiktok-video.json](examples/tiktok-video.json). ### Pattern 4: Pinterest pin (board required) Always fetch boards first, then post: ```bash # Step 1: Get boards curl -s -H "pf-[REDACTED_SECRET]" \ https://api.postfa.st/social-media/PINTEREST_ACCOUNT_ID/pinterest-boards # Step 2: Post with board ID # controls: { "pinterestBoardId": "BOARD_ID", "pinterestLink": "https://yoursite.com" } ``` See [examples/pinterest-pin.json](examples/pinterest-pin.json). ### Pattern 5: YouTube Short with tags and playlist Upload video, then post with YouTube controls: ```bash # controls object: { "youtubeIsShort": true, "youtubeTitle": "Quick Tip: Batch Your Content", "youtubePrivacy": "PUBLIC", "youtubePlaylistId": "PLxxxxxx", "youtubeTags": ["tips", "productivity", "social media"], "youtubeMadeForKids": false } ``` See [examples/youtube-short.json](examples/youtube-short.json). ### Pattern 5b: YouTube video with custom thumbnail Upload both video and thumbnail image, then reference the thumbnail key in controls: ```bash # 1. Upload thumbnail image (separate from video upload) curl -X POST https://api.postfa.st/file/get-signed-upload-urls \ -H "pf-[REDACTED_SECRET]" \ -H "Content-Type: application/json" \ -d '{ "contentType": "image/jpeg", "count": 1 }' # PUT thumbnail to signed URL # 2. Upload video (same 3-step flow as always) # 3. Create post with thumbnail key in controls: { "youtubeIsShort": false, "youtubeTitle": "Full Tutorial: Social Media Strategy", "youtubePrivacy": "PUBLIC", "youtubeThumbnailKey": "image/abc123.jpg", "youtubeTags": ["tutorial", "social media"], "youtubeMadeForKids": false } ``` Thumbnail specs: JPEG/PNG recommended, max 2MB, 1280x720 (16:9), min width 640px. Requires phone-verified YouTube channel. If thumbnail upload fails, the video still publishes without the custom thumbnail. See [examples/youtube-video-thumbnail.json](examples/youtube-video-thumbnail.json). ### Pattern 6: Google Business Profile post Always fetch locations first, then post with GBP-specific controls: ```bash # Step 1: Get locations curl -s -H "pf-[REDACTED_SECRET]" \ https://api.postfa.st/social-media/GBP_ACCOUNT_ID/gbp-locations # Step 2: Create a standard post with CTA # controls: { "gbpLocationId": "accounts/.../locations/...", "gbpTopicType": "STANDARD", "gbpCallToActionType": "LEARN_MORE", "gbpCallToActionUrl": "https://yoursite.com" } ``` Three post types: `STANDARD` (updates), `EVENT` (time-bound), `OFFER` (deals with coupons). EVENT and OFFER require `gbpEventTitle`, `gbpEventStartDate`, `gbpEventEndDate`. See [examples/gbp-standard.json](examples/gbp-standard.json), [examples/gbp-event.json](examples/gbp-event.json), and [examples/gbp-offer.json](examples/gbp-offer.json). ### Pattern 7: LinkedIn document post Documents (PDF, PPTX, DOCX) display as swipeable carousels on LinkedIn. 1. Get signed URL with `contentType: "application/pdf"` 2. PUT the file to signed URL 3. Create post using `linkedinAttachmentKey` instead of `mediaItems` ```bash # controls: { "linkedinAttachmentKey": "file/uuid.pdf", "linkedinAttachmentTitle": "Q1 Marketing Playbook" } # Note: mediaItems should be [] when using linkedinAttachmentKey ``` See [examples/linkedin-document.json](examples/linkedin-document.json). ### Pattern 8: First comment (auto-posted after publish) Add a `firstComment` to any post — it's auto-posted ~10 seconds after the main post goes live (up to 3 retries): ```json { "posts": [{ "content": "Main post text", "firstComment": "Link: https://postfa.st", "mediaItems": [], "scheduledAt": "...", "socialMediaId": "X_ID" }], "controls": {} } ``` Supported on: X, Instagram, Facebook, YouTube, Threads. NOT supported on: TikTok, Pinterest, Bluesky, LinkedIn. See [examples/x-first-comment.json](examples/x-first-comment.json). ### Pattern 9: X (Twitter) retweet Schedule a retweet — content and media are ignored: ```json { "posts": [{ "content": "", "scheduledAt": "...", "socialMediaId": "X_ID" }], "controls": { "xRetweetUrl": "https://x.com/username/status/1234567890" } } ``` See [examples/x-retweet.json](examples/x-retweet.json). ### Pattern 10: Batch scheduling (a week of posts) Schedule multiple posts at different times in a single API call (up to 15 posts per request): See [examples/batch-scheduling.json](examples/batch-scheduling.json). ## Platform-Specific Controls Pass these in the `controls` object. See [references/platform-controls.md](references/platform-controls.md) for full details. | Platform | Key Controls | |---|---| | **TikTok** | `tiktokPrivacy`, `tiktokAllowComments`, `tiktokAllowDuet`, `tiktokAllowStitch`, `tiktokIsDraft`, `tiktokIsAigc`, `tiktokBrandOrganic`, `tiktokBrandContent`, `tiktokAutoAddMusic` | | **Instagram** | `instagramPublishType` (TIMELINE/STORY/REEL), `instagramPostToGrid`, `instagramCollaborators`, `instagramTrialReelStrategy` | | **Facebook** | `facebookContentType` (POST/REEL/STORY), `facebookReelsCollaborators` | | **YouTube** | `youtubeIsShort`, `youtubeTitle`, `youtubePrivacy`, `youtubePlaylistId`, `youtubeTags`, `youtubeMadeForKids`, `youtubeCategoryId`, `youtubeThumbnailKey` | | **LinkedIn** | `linkedinAttachmentKey`, `linkedinAttachmentTitle` (for document posts) | | **X (Twitter)** | `xRetweetUrl` (retweet) | | **Pinterest** | `pinterestBoardId` (required), `pinterestLink` | | **Google Business Profile** | `gbpLocationId` (required), `gbpTopicType`, `gbpCallToActionType`, `gbpCallToActionUrl`, `gbpEventTitle`, `gbpEventStartDate`, `gbpEventEndDate`, `gbpOfferCouponCode`, `gbpOfferRedeemUrl`, `gbpOfferTerms` | | **Bluesky** | No platform-specific controls — text + images only | | **Threads** | No platform-specific controls — text + images/video | | **Telegram** | No platform-specific controls — text + images/video/mixed media | ## Helper Endpoints - **Pinterest boards**: `GET /social-media/{id}/pinterest-boards` → returns `[{ boardId, name }]` - **YouTube playlists**: `GET /social-media/{id}/youtube-playlists` → returns `[{ playlistId, title }]` - **GBP locations**: `GET /social-media/{id}/gbp-locations` → returns `[{ id, locationId, title, address, mapsUri }]`. Use `locationId` as `gbpLocationId` in controls - **Connect link**: `POST /social-media/connect-link` → returns `{ connectUrl }`. Let clients connect accounts without a PostFast account. Params: `expiryDays` (1-30, default 7), `sendEmail` (bool), `email` (required if sendEmail=true) ## Rate Limits **Global** (per API key): 60/min, 150/5min, 300/hour, 2000/day **Per-endpoint:** - `POST /social-posts`: 350/day - `GET /social-posts`: 200/hour - `GET /social-posts/analytics`: 350/hour - `POST /social-media/connect-link`: 50/hour **Platform limits:** - X (Twitter) via API: **5 posts per account per day** — do not exceed this Check `X-RateLimit-Remaining-*` headers. 429 = rate limited, check `Retry-After-*` header. For batch operations, add a 1-second delay between API calls. ## Media Specs Quick Reference | Platform | Images | Video | Carousel | |---|---|---|---| | TikTok | Carousels only | ≤250MB, MP4/MOV, 3s-10min | 2-35 images | | Instagram | JPEG/PNG | ≤1GB, 3-90s (Reels) | Up to 10 | | Facebook | ≤30MB, JPG/PNG | 1 per post | Up to 10 images | | YouTube | — | Shorts ≤3min, H.264 | — | | LinkedIn | Up to 9 | ≤10min | Up to 9, or documents (PDF/PPTX/DOCX) | | X (Twitter) | Up to 4 | — | — | | Pinterest | 2:3 ratio ideal | Supported | 2-5 images | | Google Business Profile | 1 image (JPEG/PNG, 5MB max) | Not supported | — | | Bluesky | Up to 4 | Not supported | — | | Threads | Supported | Supported | Up to 10 | | Telegram | Up to 10 | Supported | Up to 10 mixed media | ## Common Gotchas 1. **Always fetch accounts first** — `socialMediaId` is a UUID, not a platform name. Call `GET /social-media/my-social-accounts` to get valid IDs. 2. **Media MUST go through 3-step upload** — No external URLs. Always: get signed URL → PUT to S3 → use the `key` in `mediaItems`. 3. **`scheduledAt` must be in the future** — ISO 8601 UTC format. Past dates return 400. 4. **Pinterest ALWAYS requires `pinterestBoardId`** — Fetch boards first with `GET /social-media/{id}/pinterest-boards`. 5. **TikTok requires video for standard posts** — Images only work in carousels (2-35 images). 6. **LinkedIn documents use `linkedinAttachmentKey`** — NOT `mediaItems`. Set `mediaItems: []` when posting documents. 7. **Content-Type on S3 PUT must match** — The `Content-Type` header in your S3 PUT must match what you requested in `get-signed-upload-urls`. 8. **Instagram Reels need video 3-90 seconds** — Outside this range returns an error. 9. **YouTube Shorts need video under 3 minutes** — H.264 codec with AAC audio recommended. 10. **X (Twitter) has a 280 character limit** — Longer content is silently truncated. 11. **Cross-posting shares controls** — The `controls` object applies to ALL posts in the batch. Platform-irrelevant controls are ignored. 12. **X (Twitter) API limit is 5 posts/account/day** — Exceeding this risks account restrictions. 13. **`firstComment` only works on 5 platforms** — X, Instagram, Facebook, YouTube, Threads. TikTok, Pinterest, Bluesky, LinkedIn return a validation error. 14. **Retweets ignore content/media** — When `xRetweetUrl` is set, the `content` and `mediaItems` fields are ignored. 15. **LinkedIn documents support PDF, DOC, DOCX, PPT, PPTX** — Max 60MB. Cannot mix with regular media. 16. **Pagination is 0-based** — `page=0` returns the first page. Response `pageInfo.page` shows 1-based display number. 17. **Instagram trial reels require `instagramPublishType: "REEL"`** — Setting `instagramTrialReelStrategy` without it returns 400. Also cannot be combined with `instagramCollaborators`. 18. **YouTube custom thumbnails require phone verification** — `youtubeThumbnailKey` only works if the YouTube channel is phone-verified. If it fails, the video still publishes without the custom thumbnail. 19. **GBP ALWAYS requires `gbpLocationId`** — Fetch locations first with `GET /social-media/{id}/gbp-locations`. Use the `locationId` field (not `id`). 20. **GBP supports only 1 image** — No video, no carousels. JPEG/PNG, max 5MB. 21. **GBP EVENT/OFFER posts require dates** — `gbpEventStartDate` and `gbpEventEndDate` are required when `gbpTopicType` is `EVENT` or `OFFER`. 22. **GBP content limit is 1,500 characters** — Shorter than most platforms. 23. **GBP posts expire** — Standard posts auto-expire after 6 months. Event/Offer posts expire at their end date. 24. **`coverTimestamp` is milliseconds** — e.g., `"5000"` = 5 seconds into the video. Not seconds. 25. **`coverImageKey` platform limits** — Instagram Reels: JPEG only, max 8MB. Facebook Reels: any format, max 10MB. Pinterest video: JPEG/PNG. NOT supported on TikTok (use `coverTimestamp`) or YouTube (use `youtubeThumbnailKey`). 26. **Facebook Reels don't support `coverTimestamp`** — Only `coverImageKey` works for FB Reel covers. `coverTimestamp` is ignored. 27. **Disconnected accounts reject scheduled posts** — If an account's `connectionStatus` is `DISABLED`, scheduling to it returns `400 socialMediaDisconnected`. Drafts still work. Check `connectionStatus` from `my-social-accounts` first, and watch `lastError.code` for `MISSED_DISCONNECTED` (post was due while the account was down) or `MISSED_NOT_PUBLISHED`. ## Troubleshooting ### 403 "Missing organizationId or activeWorkspaceId" This is the most common error. It means the API didn't recognize your key. Check these in order: 1. **Wrong header name.** The header must be exactly `pf-api-key`. Not `Authorization: Bearer`, not `x-api-key`, not `api-key`. Example: ```bash # Correct curl -H "pf-[REDACTED_SECRET]" https://api.postfa.st/social-media/my-social-accounts # Wrong — these all return 403 curl -H "[REDACTED_SECRET]" ... curl -H "x-[REDACTED_SECRET]" ... ``` 2. **Env var not set.** If `$POSTFAST_API_KEY` isn't set in your shell, the literal string `$POSTFAST_API_KEY` gets sent as the key value. Verify it's set: ```bash echo $POSTFAST_API_KEY # Should print a 44-character base64 string ending with "=" ``` If empty, re-export it. If you're using a `.env` file, make sure your tool actually loads it (dotenv, direnv, etc.). Shell quoting matters: use double quotes around the value if it contains special characters. 3. **Regenerated key.** Each time you click "Generate API Key" in PostFast settings, the previous key is **permanently invalidated**. Only regenerate if the old key is compromised. If you regenerated and are still using the old key, that's why it fails. 4. **Wrong key entirely.** Your PostFast API key is a 44-character base64 string ending with `=`. Don't confuse it with keys from other services (OpenAI `sk-proj-...`, Stripe `sk_live_...`, etc.). ### 401 Invalid or missing API key The `pf-api-key` header is either missing from the request or the value is empty. Double-check that your HTTP client is actually sending the header (some tools strip custom headers on redirects). ### 429 Rate limit exceeded You've hit the per-workspace rate limit. Check the `Retry-After-Minute`, `Retry-After-5Minutes`, `Retry-After-Hour`, or `Retry-After-Day` response header for when you can retry. Limits: 60/min, 150/5min, 300/hour, 2,000/day. ## Supporting Resources **Reference docs:** - [references/api-reference.md](references/api-reference.md) — Complete API endpoint reference with response examples - [references/platform-controls.md](references/platform-controls.md) — All platform-specific controls with types and defaults - [references/media-specs.md](references/media-specs.md) — Media size, format, and dimension limits per platform - [references/upload-flow.md](references/upload-flow.md) — Detailed media upload walkthrough **Ready-to-use examples:** - [examples/EXAMPLES.md](examples/EXAMPLES.md) — Index of all examples - [examples/cross-platform-post.json](examples/cross-platform-post.json) — Multi-platform posting - [examples/tiktok-video.json](examples/tiktok-video.json) — TikTok with privacy settings - [examples/tiktok-carousel.json](examples/tiktok-carousel.json) — TikTok image carousel - [examples/tiktok-aigc-video.json](examples/tiktok-aigc-video.json) — TikTok AI-generated video with AIGC label - [examples/draft-post.json](examples/draft-post.json) — Generic PostFast draft (any platform, no `scheduledAt`) - [examples/tiktok-app-draft.json](examples/tiktok-app-draft.json) — TikTok app draft (`tiktokIsDraft` control, pushes to TikTok app inbox) - [examples/instagram-reel.json](examples/instagram-reel.json) — Instagram Reel - [examples/instagram-reel-cover.json](examples/instagram-reel-cover.json) — Instagram Reel with custom cover image - [examples/instagram-story.json](examples/instagram-story.json) — Instagram Story - [examples/instagram-carousel.json](examples/instagram-carousel.json) — Instagram carousel - [examples/instagram-trial-reel.json](examples/instagram-trial-reel.json) — Instagram trial reel (non-followers first) - [examples/facebook-reel.json](examples/facebook-reel.json) — Facebook Reel - [examples/facebook-story.json](examples/facebook-story.json) — Facebook Story - [examples/youtube-short.json](examples/youtube-short.json) — YouTube Short with tags - [examples/youtube-video-thumbnail.json](examples/youtube-video-thumbnail.json) — YouTube video with custom thumbnail - [examples/pinterest-pin.json](examples/pinterest-pin.json) — Pinterest with board - [examples/linkedin-document.json](examples/linkedin-document.json) — LinkedIn document post - [examples/x-retweet.json](examples/x-retweet.json) — X scheduled retweet - [examples/x-first-comment.json](examples/x-first-comment.json) — X post with auto first comment - [examples/threads-carousel.json](examples/threads-carousel.json) — Threads image carousel - [examples/batch-scheduling.json](examples/batch-scheduling.json) — Week of scheduled posts - [examples/gbp-standard.json](examples/gbp-standard.json) — Google Business Profile standard update with CTA - [examples/gbp-event.json](examples/gbp-event.json) — Google Business Profile event post - [examples/gbp-offer.json](examples/gbp-offer.json) — Google Business Profile offer with coupon code - [examples/telegram-mixed-media.json](examples/telegram-mixed-media.json) — Telegram mixed media - [examples/pinterest-analytics.json](examples/pinterest-analytics.json) — Pinterest pin analytics with extras (pin_clicks, outbound_clicks, save_rate, video metrics) ## Quick Reference ``` # Auth Header: pf-[REDACTED_SECRET] # List accounts GET /social-media/my-social-accounts # Schedule post POST /social-posts { posts: [{ content, mediaItems, scheduledAt, socialMediaId, firstComment? }], status?, approvalStatus?, controls: {} } # Draft post (no scheduledAt needed) POST /social-posts { posts: [...], status: "DRAFT", controls: {} } # List posts (page is 0-based, limit max 50) GET /social-posts?page=0&limit=20 GET /social-posts?page=0&limit=50&platforms=X,LINKEDIN&statuses=SCHEDULED&from=2026-06-01T00:00:00Z&to=2026-06-30T23:59:59Z # Delete post DELETE /social-posts/:id # Upload media (3 steps) POST /file/get-signed-upload-urls { contentType, count } PUT <signedUrl> (raw file, matching Content-Type) # then use key in mediaItems # Pinterest boards GET /social-media/:id/pinterest-boards # YouTube playlists GET /social-media/:id/youtube-playlists # GBP locations GET /social-media/:id/gbp-locations # Post analytics (published posts with metrics) GET /social-posts/analytics?startDate=...&endDate=...&platforms=... # Connect link (for clients) POST /social-media/connect-link { expiryDays?, sendEmail?, email? } ``` ## Tips for the Agent - Always call `my-social-accounts` first to get valid `socialMediaId` values. - For media posts, complete the full 3-step upload flow (signed URL → S3 PUT → create post). - `scheduledAt` must be ISO 8601 UTC and in the future. - Pinterest always requires `pinterestBoardId` — fetch boards first. - LinkedIn documents use `linkedinAttachmentKey` instead of `mediaItems`. - For carousels, include multiple items in `mediaItems` with sequential `sortOrder`. - Video cover images: use `coverImageKey` in `mediaItems` for IG Reels, FB Reels, Pinterest video. Use `coverTimestamp` (milliseconds) for TikTok. YouTube uses `youtubeThumbnailKey` in controls. - When cross-posting, adjust content length for each platform's limits (X: 280, Bluesky: 300, TikTok: 2200, GBP: 1500). - If the user doesn't specify a time, suggest tomorrow at 9:00 AM in their timezone. - Batch up to 15 posts per API call for efficiency. - Use `firstComment` for CTAs and links — keeps the main post clean and gets better engagement. - X (Twitter) allows only 5 posts per account per day via API — warn the user if they're batching many X posts. - For draft posts, set `status: "DRAFT"` and omit `scheduledAt` — the user can finalize in the PostFast dashboard. - GBP always requires `gbpLocationId` — fetch locations first with `GET /social-media/{id}/gbp-locations`. - GBP supports 3 post types: STANDARD (default), EVENT, and OFFER. EVENT/OFFER need start and end dates. - GBP only supports 1 image (no video, no carousels) and has a 5-post/day limit. - Use `GET /social-posts` with `from`/`to` filters to check what's already scheduled before adding more. - Check `connectionStatus` before scheduling — `DISABLED` accounts reject scheduled posts but still accept drafts. The user reconnects from the dashboard to resolve it.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)