Getting Started
Posthive Documentation
Schedule posts to multiple platforms from one interface with built-in MCP support for AI agents.
New to open-source schedulers? Read our guide to the best open-source social media schedulers in 2026 →
Quick start
Get Posthive running locally in under five minutes.
1. Clone the repository
git clone https://github.com/AstaBlackClove/posthive.git
cd posthive2. Copy environment files
cp apps/api/.env.example apps/api/.env
# Edit apps/api/.env and fill in the required values3. Run database migrations
cd apps/api
pnpm db:migrate4. Start the dev server
# From the repo root
pnpm devThis starts both the API on http://localhost:3001 and the web app on http://localhost:3000 in parallel.
Installation
Prerequisites
- Node.js ≥ 20
- pnpm ≥ 9 (npm i -g pnpm)
- Redis Upstash free tier or Railway Redis (required for BullMQ job queue)
Clone and install
git clone https://github.com/AstaBlackClove/posthive.git
cd posthive
pnpm installThe monorepo uses pnpm workspaces. Running pnpm install at the root installs dependencies for both apps/api and apps/web.
Environment variables
All configuration lives in apps/api/.env. Copy .env.example and fill in the values below.
| Variable | Required | Description |
|---|---|---|
| DATABASE_URL | Yes | SQLite path (file:./dev.db) for dev, or a Postgres connection string for prod. |
| ENCRYPTION_KEY | Yes | 64-char hex string. Used for AES-256-GCM encryption of stored credentials. Never change after data is written. |
| REDIS_URL | Yes | Redis connection string (e.g. rediss://...) for BullMQ job queue. |
| JWT_ACCESS_SECRET | Yes | 64-char hex. Signs short-lived access tokens. |
| JWT_REFRESH_SECRET | Yes | 64-char hex. Signs long-lived refresh tokens. |
| WEB_URL | Yes | URL of the web app. Use http://localhost:3000 in dev so auth cookies stay stable across tunnel restarts. |
| PUBLIC_API_URL | Meta only | Public HTTPS URL of the API. Meta fetches images from this URL, so it must be reachable from the internet. |
| ENABLE_BILLING | No | Set to true to enable Dodo Payments billing. Defaults to off for self-hosters. |
| AUTH_PROVIDER | No | local (default) or supabase. Switches the auth backend. |
| GOOGLE_CLIENT_ID | No | Google OAuth client ID. Enables "Sign in with Google" on the login and register pages. |
| GOOGLE_CLIENT_SECRET | No | Google OAuth client secret. Required alongside GOOGLE_CLIENT_ID. |
| GOOGLE_REDIRECT_URI | No | Callback URL registered in Google Cloud Console. Defaults to http://localhost:3001/auth/google/callback. |
| ENABLE_ANALYTICS | No | Set to true to enable visitor session + event tracking. Off by default self-hosters collect no data. |
| ADMIN_EMAIL | No | Your email address. Only this account can access /admin. |
| ADMIN_PIN | No | Second-factor PIN for /admin. Validated server-side never exposed to the browser. Set in API env only. |
| ANTHROPIC_API_KEY | No | Anthropic API key for AI caption features (Fix Grammar, Expand, Polish, etc.). Get one at console.anthropic.com. Self-hosters supply their own key. |
| TIKTOK_CLIENT_KEY | TikTok only | TikTok app client key from developers.tiktok.com. |
| TIKTOK_CLIENT_SECRET | TikTok only | TikTok app client secret. |
| TIKTOK_REDIRECT_URI | TikTok only | OAuth callback URL. Must match exactly what is registered in the TikTok developer portal. |
Google Sign-In
Adds a Continue with Google button to the login and register pages. When a user signs in with Google using an email that already has a password-based account, Posthive merges them automatically — no duplicate account is created.
1. Create a Google OAuth client
- Go to console.cloud.google.com and open (or create) a project.
- Navigate to APIs & Services → OAuth consent screen. Set User Type to External, fill in the app name and your email, then save.
- Go to APIs & Services → Credentials → Create Credentials → OAuth client ID.
- Choose Web application. Under Authorized redirect URIs add your callback URL (e.g. https://your-domain.com/auth/google/callback for prod, http://localhost:3001/auth/google/callback for dev).
- Copy the Client ID and Client Secret.
2. Add env vars
Add to apps/api/.env:
GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-client-secret"
GOOGLE_REDIRECT_URI="https://your-domain.com/auth/google/callback"How account merging works
When a user clicks Continue with Google, Posthive looks up the Google account's email in the database:
- Email not found — new account created, 14-day trial started, user lands on onboarding.
- Email matches an existing account — user is logged in to that account. The original password remains valid. Both sign-in methods work going forward.
Platforms
Bluesky
Bluesky uses app passwords no OAuth flow required. Connection is straightforward and does not need a public callback URL.
How to connect
- Go to bsky.app → Settings → Privacy and Security → App Passwords.
- Create a new app password and copy it.
- Open the Accounts page in Posthive, click Connect Bluesky.
- Enter your Bluesky handle (e.g. yourname.bsky.social) and the app password.
Posthive stores the app password encrypted with AES-256-GCM. Your main account password is never used or stored.
Threads
Threads uses Meta OAuth 2.0. You need a Meta Developer app with the Threads use case enabled.
Setup
- Go to developers.facebook.com and create an app.
- Add the Threads API use case to your app.
- Add a redirect URI matching THREADS_REDIRECT_URI in your env must be a public HTTPS URL.
- Copy your App ID and App Secret into THREADS_APP_ID and THREADS_APP_SECRET.
- Click Connect Threads on the Accounts page and complete the OAuth flow.
Threads tokens expire every 60 days. Posthive automatically refreshes them before each scheduled post.
Instagram publishing requires a Professional (Business or Creator) account linked to a Facebook Page, and a Meta Developer app with the Instagram product enabled.
Supported media types
- Post - single image or carousel (up to 10 images on Pro/Team)
- Reel - short video (Pro/Team plans only)
- Story - image or video story (Pro/Team plans only)
Setup
- Create a Meta Developer app and add the Instagram product.
- Set your redirect URI to INSTAGRAM_REDIRECT_URI (must be public HTTPS).
- Fill in INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET, and PUBLIC_API_URL.
- Click Connect Instagram on the Accounts page.
Token lifetimes
On a successful connection, Posthive exchanges your short-lived token (1 hour) for a long-lived token (60 days) that is automatically refreshed in the background — you should never need to reconnect.
LinkedIn uses OAuth 2.0 via the LinkedIn Developer platform.
Setup
- Create an app at developer.linkedin.com.
- Add the Share on LinkedIn and Sign In with LinkedIn using OpenID Connect products.
- Add your callback URL under Auth → Authorized redirect URLs.
- Set LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET in your env.
- Click Connect LinkedIn on the Accounts page.
Mastodon
Posthive works with any Mastodon instance. You register an application within your own instance and paste the credentials into Posthive.
How to connect
- Log in to your Mastodon instance (e.g. mastodon.social).
- Go to Settings → Development → New application.
- Give it a name, set the redirect URI to your Posthive callback URL, and enable the write:statuses and write:media scopes.
- Copy the Client key and Client secret.
- Click Connect Mastodon in Posthive, enter your instance URL and the credentials.
Pixelfed
Pixelfed is a federated, open-source photo-sharing platform built on ActivityPub — an Instagram alternative for the fediverse. Posthive connects via OAuth 2.0 and works with any Pixelfed instance (pixelfed.social, pixelfed.uno, gram.social, or your own self-hosted server).
How to connect
- Create a free account on any Pixelfed instance (e.g. pixelfed.social or pixelfed.uno).
- Set PIXELFED_REDIRECT_URI in your env to https://your-domain.com/auth/pixelfed/callback. No client ID/secret needed — Posthive registers an OAuth app dynamically on each instance.
- Go to Accounts, click Connect Pixelfed, enter your instance URL (e.g. pixelfed.social), and approve access.
Notes
- Image required — every Pixelfed post must have at least one image. The composer warns you if you try to schedule without one.
- Caption up to 2,001 chars, up to 4 images per post, alt text supported.
- Audience controls: Public, Unlisted, or Followers Only — set per post in the composer.
- NSFW / Sensitive toggle — blurs media behind a content warning on the instance.
- First comment scheduling is supported.
- You can connect multiple accounts from different instances simultaneously.
YouTube
Posthive publishes to YouTube as Shorts (or regular videos your choice per post) using Google OAuth 2.0 and the YouTube Data API v3. Every post requires a video attached.
How to connect
- Create a project at console.cloud.google.com and enable the YouTube Data API v3.
- Configure the OAuth consent screen add the youtube.upload, youtube.readonly, and youtube.force-ssl scopes, and add your own Google account under Audience → Test users while the app is unverified.
- Create an OAuth client (Web application) and copy the Client ID and Secret into YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET.
- Click Connect YouTube on the Accounts page and authorize.
Shorts vs. regular video
In Compose, the YouTube section has a Short / Video toggle. YouTube classifies Shorts by the video file itself vertical (9:16) and 60 seconds or under is the reliable threshold. Posthive auto-appends #Shorts to the description when "Short" is selected, but that tag alone does nothing if the video doesn't already qualify by aspect ratio and duration. Posthive checks the attached video's dimensions and warns you in the UI if it won't actually classify as a Short.
Title and description are separate dedicated fields (not the shared Post box other platforms use) title is capped at 100 characters, description at 5,000. Selecting only YouTube accounts hides the shared Post box entirely since it isn't used.
Facebook Pages
Posthive publishes to Facebook Pages you manage via the Graph API v21.0. Text, single photo, multi-photo carousel, and video posts are all supported. The Facebook API does not allow posting to personal profiles a Page is required.
How to connect
- At developers.facebook.com, open your Meta app and add the "Manage everything on your Page" use case this grants pages_manage_posts, pages_show_list, and pages_read_engagement.
- Under Facebook Login for Business → Settings, add your callback URL as a valid OAuth redirect URI: https://your-domain/auth/facebook/callback.
- Copy the main App ID and App secret into FACEBOOK_APP_ID / FACEBOOK_APP_SECRET (same values as Threads if using the same Meta app).
- Create a Facebook Page at facebook.com/pages/create if you don't have one.
- Click Connect Facebook Page on the Accounts page and authorise all Pages you admin will be connected automatically.
Posthive publishes Pins to Pinterest using the Pinterest API v5. Every Pinterest post requires an image — Pinterest is a visual platform and rejects posts without one.
How it works
- Title — first line of your post text (max 100 chars).
- Description — remaining lines (max 500 chars).
- Link — if your post contains a URL, it is attached as the Pin destination.
- Board — the Pin is created on the default board selected at connect time.
Environment variables
| Variable | Required | Description |
|---|---|---|
| PINTEREST_CLIENT_ID | Yes | OAuth app Client ID from the Pinterest developer portal. |
| PINTEREST_CLIENT_SECRET | Yes | OAuth app Client Secret. |
| PINTEREST_REDIRECT_URI | Yes | Must match the redirect URI registered in the Pinterest app settings. |
| PINTEREST_SANDBOX | No | Set to true to use the Pinterest sandbox API. Required for Trial access apps. |
| PINTEREST_SANDBOX_TOKEN | No | Manually generated sandbox token from the Pinterest developer dashboard. When set, bypasses the OAuth code exchange entirely (required for Trial apps whose token exchange is restricted). |
Moving to production (after Standard access approval)
- Remove PINTEREST_SANDBOX=true and PINTEREST_SANDBOX_TOKEN from your env.
- The Pinterest connect button on the Accounts page will be re-enabled in the next release.
- Users connect via the normal OAuth flow — no manual token required.
Telegram
Posthive publishes to Telegram channels via the Telegram Bot API. No OAuth flow is required — connection uses a bot token you generate yourself. Text, images (up to 10), and video posts are supported. First comments are not available on Telegram channels.
How to connect
- Open Telegram and message @BotFather → send /newbot → follow the prompts. BotFather gives you a bot token (looks like 123456789:ABCdef...).
- Create a Telegram channel — public or private.
- Add your bot to the channel as an Administrator: open the channel → Administrators → Add Administrator → search your bot → enable Post Messages → Done.
- Go to Accounts in Posthive and click Connect Telegram Channel.
- Paste the bot token and your channel identifier, then click Connect.
Channel identifier
- Public channels — use the username: @mychannel
- Private channels — use the numeric chat ID: -1001234567890. To find it, forward any message from the channel to @userinfobot on Telegram.
What gets posted
- Text only — sent as a plain message.
- Single image — sent via sendPhoto with caption.
- Multiple images — sent as a media group (sendMediaGroup, up to 10).
- Video — sent via sendVideo with caption.
Nostr
Posthive publishes Kind 1 notes to Nostr relays using your keypair. No OAuth, no app approval — just paste your nsec private key. Text and images are supported. Images are appended as URLs in the note content and use NIP-92 imeta tags for clients that support inline rendering.
Setup
- Go to Accounts in Posthive and click Connect Nostr.
- Paste your nsec1... private key — or click Generate a new keypair to create a fresh one.
- Your public key (npub) and profile photo are fetched automatically from relays.
Relays
Posts are published to four well-known high-uptime relays by default:
- wss://relay.damus.io
- wss://relay.nostr.band
- wss://nos.lol
- wss://relay.snort.social
Images
Images must be publicly accessible URLs — they are appended to the note text so Nostr clients can fetch and render them inline. Make sure PUBLIC_API_URL is set to your public API URL (not localhost) so uploaded images resolve correctly on the Nostr network.
Discord
Posthive posts to Discord channels via a webhook created automatically when you connect a channel. Text, images (up to 10), and video are supported. First comments post as replies to the original message.
Setup
- Go to discord.com/developers → New Application → name it Posthive.
- Go to the Bot tab → click Add Bot → copy the bot token.
- Go to OAuth2 → General → copy Client ID and Client Secret. Add your redirect URI.
- Add the following to your .env:
DISCORD_CLIENT_ID="your-client-id" DISCORD_CLIENT_SECRET="your-client-secret" DISCORD_BOT_TOKEN="your-bot-token" DISCORD_REDIRECT_URI="https://your-domain.com/auth/discord/callback"
- Go to Accounts in Posthive → click Connect Discord.
- Authorise the bot on your Discord server, then pick a channel in the Posthive channel picker.
Bot permissions required
- View Channels — to access the channel
- Send Messages — to post
- Attach Files — for images and video
- Read Message History — for replies
- Manage Webhooks — to auto-create the posting webhook
Tumblr
Posthive posts to your primary Tumblr blog using the NPF (Neue Post Format) API. Text and images are supported. Tumblr uses OAuth 1.0a — tokens never expire so you only connect once.
- Go to tumblr.com/oauth/apps → Register application. No review required.
- Set Default callback URL to your production API domain +
/auth/tumblr/callback. - Add to your
.env:
TUMBLR_CONSUMER_KEY="your-consumer-key" TUMBLR_CONSUMER_SECRET="your-consumer-secret" TUMBLR_REDIRECT_URI="https://your-domain.com/auth/tumblr/callback"
- Go to Accounts in Posthive → click Connect Tumblr → approve the OAuth prompt.
Lemmy
Posthive posts to a Lemmy community on any public instance. Lemmy uses username/password authentication — a fresh JWT is obtained per post so no token expiry issues exist.
Connect
- Go to Accounts in Posthive → click Connect Lemmy.
- Enter your instance URL (e.g. https://lemmy.world), username, password, and the community name (e.g. technology — no ! prefix needed).
- Click Connect. Posthive verifies credentials immediately.
Supported content
- Post text becomes the link post body or a text post if no URL is present.
- The first image (if any) is used as the post URL so it appears as a link preview in Lemmy.
- First comment automation is not supported on Lemmy.
TikTok
Posthive posts videos to TikTok via the Content Posting API v2. Text-only and image posts are not supported by TikTok's API — a video file (mp4 or mov) is required.
- Go to developers.tiktok.com → create an app.
- Add products: Login Kit and Content Posting API. Enable Direct Post under Content Posting API.
- Set the redirect URI to
https://your-domain.com/auth/tiktok/callback. - Copy your client key and secret into your API env:
TIKTOK_CLIENT_KEY="your-client-key" TIKTOK_CLIENT_SECRET="your-client-secret" TIKTOK_REDIRECT_URI="https://your-domain.com/auth/tiktok/callback"
- Go to Accounts in Posthive → click Connect TikTok.
Features
AI caption assist
The composer and post editor include an AI button that rewrites your caption using Claude. Seven actions are available:
- Fix Grammar — corrects spelling, grammar, and punctuation.
- Make it Concise — shortens while keeping the core message.
- Expand — adds detail and makes the caption more engaging.
- Rephrase — rewrites with the same meaning in a fresh voice.
- Improve Structure — improves readability and flow.
- Simplify Language — makes the caption easy to understand.
- Polish my Caption — makes it compelling and professional.
Self-hosters must set ANTHROPIC_API_KEY in their API env. Get a key at console.anthropic.com.
Scheduling posts
The Compose page is where you write and schedule posts. Everything happens in a single panel no multi-step wizard.
Steps
- Select one or more connected accounts from the account picker at the top.
- Write your post in the text area. The character counter updates per-platform.
- Optionally attach images or video using the media button.
- Pick a scheduled time using the date-time picker. You can also post immediately.
- Click Schedule. The post is enqueued and will fire at the chosen time.
Repeat posts
Repeat scheduling automatically creates the next occurrence of a post every time the current one publishes. The chain continues indefinitely until an optional end date is reached — no manual re-scheduling needed.
How to set it up
- Open the Compose page and write your post.
- In the bottom toolbar, click Repeat Post… and choose Every Day, Every Week, or Every Month.
- Optionally set an until end date. Leave it blank for an indefinite repeating post.
- Schedule as normal. The first post fires at your chosen time; each subsequent post fires exactly +1 day / +7 days / +1 month later.
How it works
When a repeat post publishes successfully, the worker automatically creates the next job in the DB and enqueues it in BullMQ. The new job has a repeatParentId pointing back to the original post so the full chain is traceable. If a post fails, the chain does not advance — the failed job stays failed and no child is created.
Stopping a repeat chain
Delete the upcoming scheduled post from the Posts page. Because only one future job exists at a time, deleting it stops the chain completely.
Via API / MCP
Pass repeat_interval ("daily" | "weekly" | "monthly") and optionally repeat_ends_at (ISO 8601) to create_post.
Design templates
A template is the design team's reference design for one content type: a content pillar plus a content category (for example Promotional, Case Study). There is one template per category. It holds a Figma link (figma.com/design, file, proto or board), plus an optional preview image.
Managing templates (design team)
- Open Templates. Each pillar lists its categories; empty ones show + Upload.
- Pick the pillar and category, then paste the Figma link. The preview image is optional.
- Optionally add a preview image so others recognise the layout. Replacing or deleting a template removes its old design file.
Using a template in Compose
- In the Content type section, choose the pillar and category, or click Pick from templates. Use template in the library does the same.
- Only the pillar and category are filled in. Your media, caption and schedule are not touched.
- The design reference card shows Open in Figma and Download file. Build the final design from it, then upload the finished images to the post.
Template files are never attached to a post. Only the design team can upload, replace or delete templates; Admin and Marketing view and use them.
Import Plan
Import Plan turns a content planning spreadsheet into planned posts in Posthive. Open it from the Posts page (Import Plan button in the top bar).
Supported files
CSV (UTF-8) or .xlsx, up to 5 MB and 1.000 data rows. For XLSX the header row is detected automatically, so extra title rows above the data are fine. Download a ready-made template from the Format Guide tab on the Import Plan page.
Required columns
- Content Title — the post title, used in the calendar.
- Post Date — publish date. DD-MM-YYYY is preferred; YYYY-MM-DD, DD/MM/YYYY and YYYY/MM/DD are also accepted. Time defaults to 09:00 WIB.
- Platform — LinkedIn, Instagram, Website, or Mirroring (LI&Ig).
Platform values
- LinkedIn or LI — LinkedIn.
- Instagram or IG — Instagram.
- Mirroring (LI&Ig) — LinkedIn and Instagram together.
- Website — saved as a draft without a social target.
Optional columns
All optional, and all can be left empty: Content Pillar, Content Category,Link Copywrite (design copy), Caption (final caption),Aset Pendukung, PIC Content,Content Review / Acc by, PIC Design,Progress Content, Link Output, and Status.
Link Copywrite is stored raw as the design copy and Caption as the caption. Keep them separate from the start rather than reusing one column for both.
Two-row headers
Client workbooks often repeat the required labels on a second row, for example Category aboveContent Pillar and Content Category. Both rows are read together, so that layout is supported without editing the file.
Example CSV
Content Title,Post Date,Platform
Slope Stability Before the Rain Peaks,22-10-2026,Mirroring (LI&Ig)
Q4 Marketing Campaign Highlights,30-10-2026,LinkedIn / LI
Summer Vacation Photos,25-10-2026,Instagram / IG
Website Refresh Announcement,12-11-2026,WebsitePreview and importing
- Drop a CSV or XLSX file, or paste CSV into the editor on the left.
- Click Preview CSV — every row is validated against columns, dates, platforms, and duplicates.
- Review the table. Rows are marked ready, invalid, or duplicate; duplicates inside the same file are caught automatically.
- Select the rows you want and click Import. Selected valid rows become drafts on the Posts page.
Dates already in the past are kept as archived drafts, and rows marked Published in the source file are still imported as drafts — nothing is published automatically.
Calendar view
The Posts page (/jobs) has both a list view and a calendar view. Toggle between them with the view switcher in the top-right corner.
The calendar supports month, week, and day modes. Pending posts can be dragged to a new time slot to reschedule them without opening the edit dialog.
First comment
The first comment field lets you attach a reply that is posted immediately after the main post goes live. This is commonly used to add hashtags without cluttering the main post body, or to add a thread continuation.
The comment is published per-platform each connected account gets its own first comment published to that platform.
Per-platform overrides
Available on Pro and Team plans. Per-platform overrides let you customise the post text for individual accounts without creating separate posts.
Click the Customize button next to any selected account in the Compose page. A dialog opens where you can edit the content independently for that account. Accounts without an override use the main post body.
Media uploads
Posthive supports image and video attachments. Images can be attached by clicking the media button, dragging files into the compose area, or pasting from the clipboard.
- Creator plan: up to 4 images per post (carousel).
- Pro / Team plans: up to 10 images per carousel.
- Alt text is supported click any thumbnail to add descriptive text for accessibility.
- In development, files are stored on local disk. In production, set STORAGE_PROVIDER=r2 (Cloudflare R2, recommended) or STORAGE_PROVIDER=supabase and configure the relevant env vars.
Content Library
The Content Library lets you build an evergreen queue of posts that drip out automatically at your configured time slots — no manual scheduling needed. Upload a CSV once and posts go out on autopilot for weeks or months.
Available on Creator, Pro, and Team plans (not available during trial).
How it works
- Go to Library in the sidebar and create a new library.
- Choose the social accounts to post to, your timezone, posts per day, and time slots.
- Upload a CSV of content items.
- The drip engine fires every 5 minutes and schedules one post per matching time slot — automatically.
- When all items are posted the library is marked Exhausted. Upload more content to reactivate it.
CSV format
Each row is one post. Supported columns:
text,commentText
"Your post content here","Optional first comment"- text — required. The post body.
- commentText — optional. Auto-posted as first comment after the post publishes.
Plan limits
- Creator: 1,000 items across all libraries.
- Pro: 20,000 items.
- Team: 100,000 items.
Tips
- Create multiple libraries for different content themes or languages.
- Uploading new content to an exhausted library automatically reactivates it.
- Delete and recreate a library to start fresh with a new content set.
- The library respects your timezone — time slots are in local time.
Self-hosting
Docker setup
Self-host Posthive on any Linux VPS, Raspberry Pi, or cloud VM — no Node.js or pnpm needed. Docker Compose starts Postgres, Redis, the API, and the web app with one command. All configuration lives in a single .env file at the project root.
Prerequisites
- Docker Engine 24+ with the Compose v2 plugin (docker compose, not docker-compose)
- A domain with two A records pointing at your server: yourdomain.com → web, api.yourdomain.com → API
- A reverse proxy (Caddy or nginx) for HTTPS termination
Step 1 — Clone
git clone https://github.com/AstaBlackClove/posthive.git
cd posthiveStep 2 — Create your .env
.env.docker is a ready-made template with only the vars you need for Docker. Copy it to .env at the project root — this single file configures all four containers.
cp .env.docker .envStep 3 — Generate secrets
Run this command three times and paste each output into ENCRYPTION_KEY, JWT_ACCESS_SECRET, and JWT_REFRESH_SECRET:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Step 4 — Set your URLs
Edit .env and fill in your domain. These two vars are baked into the Next.js build — set them correctly before the first build.
POSTGRES_PASSWORD=strong_random_password
ENCRYPTION_KEY=<64-char hex>
JWT_ACCESS_SECRET=<64-char hex>
JWT_REFRESH_SECRET=<64-char hex>
WEB_URL=https://yourdomain.com
PUBLIC_API_URL=https://api.yourdomain.comEverything else in .env.docker is optional — uncomment only the platforms you want to enable. Redis and Postgres connection strings are set automatically by Docker Compose; you do not need to set them manually.
Step 5 — Add platform OAuth vars
For each social platform you want to enable, uncomment its block in .env and set the redirect URI to https://api.yourdomain.com/auth/<platform>/callback. For example:
THREADS_APP_ID=your-app-id
THREADS_APP_SECRET=your-app-secret
THREADS_REDIRECT_URI=https://api.yourdomain.com/auth/threads/callbackBluesky, Telegram, Nostr, Mastodon, Pixelfed, Lemmy, and Discord need no app registration — they work out of the box.
Step 6 — Build and start
docker compose up -d --buildFirst build takes 2–5 minutes. Once done, open your domain and register your account. Billing is disabled by default — all features are unlocked.
| Container | Default port | Purpose |
|---|---|---|
| web | 3000 | Next.js frontend |
| api | 3001 | Fastify backend + BullMQ worker |
| db | 5432 (internal) | Postgres 16 |
| redis | 6379 (internal) | BullMQ job queue |
Custom domain
Set your public URLs in .env before building, then point a reverse proxy at the containers.
WEB_URL=https://yourdomain.com
PUBLIC_API_URL=https://api.yourdomain.comRebuild after changing URLs:
docker compose up -d --buildCaddy (recommended — auto HTTPS)
yourdomain.com {
reverse_proxy localhost:3000
}
api.yourdomain.com {
reverse_proxy localhost:3001
}nginx
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Persistent uploads
By default, uploaded images live inside the container and are lost on rebuild. Add a named volume in docker-compose.yml to persist them:
services:
api:
...
volumes:
- uploads:/app/apps/api/uploads
volumes:
postgres_data:
redis_data:
uploads: # add thisFor production, use Cloudflare R2 (recommended, zero egress fees) or Supabase Storage — set STORAGE_PROVIDER=r2 or STORAGE_PROVIDER=supabase with the relevant env vars. Files are stored off-server and survive rebuilds automatically.
Updating
git pull
docker compose up -d --buildDatabase migrations run automatically on API startup — no manual step needed.
Railway / Render / Fly.io
If you prefer a managed platform over a VPS, deploy the source repo and set these commands:
# Build command
pnpm install --frozen-lockfile --filter api... && pnpm --filter api exec prisma generate && pnpm --filter api build
# Start command
cd apps/api && node_modules/.bin/prisma migrate deploy && node dist/index.jsDatabase
Posthive uses Prisma 5 with SQLite in development and Postgres in production. Switching is a one-line change in the schema.
// apps/api/prisma/schema.prisma
datasource db {
provider = "postgresql" // change from "sqlite"
url = env("DATABASE_URL")
}Run migrations after any schema change:
cd apps/api
pnpm db:migrateRedis
Redis is used exclusively for the BullMQ job queue that powers scheduled post delivery. No application state is stored in Redis it is safe to flush between deploys as long as no posts are currently queued.
Options
- Upstash - free tier is sufficient for most self-hosters. Use the rediss:// TLS URL.
- Railway Redis - add the Redis plugin to your Railway project and copy the connection string.
- Self-hosted - any Redis 6+ instance works.
REDIS_URL="rediss://default:<password>@<host>:<port>"Storage
Uploaded media can be stored locally (dev), in Cloudflare R2 (recommended for production), or in Supabase Storage. Switch with the STORAGE_PROVIDER env variable.
Local storage (default)
STORAGE_PROVIDER=localFiles are written to apps/api/uploads/. Not recommended for production — files are lost on redeploy.
Cloudflare R2 (recommended)
Zero egress fees. Files survive redeploys. Works with any S3-compatible client.
- Create a Cloudflare account and open R2 in the dashboard.
- Create a bucket (e.g. posthive-uploads).
- Under Settings → Public access, enable the public bucket URL (or connect a custom domain).
- Go to Manage R2 API tokens and create a token with Object Read & Write on your bucket. Copy the Account ID, Access Key ID, and Secret Access Key.
STORAGE_PROVIDER=r2
R2_ACCOUNT_ID="your-cloudflare-account-id"
R2_ACCESS_KEY_ID="your-r2-access-key-id"
R2_SECRET_ACCESS_KEY="your-r2-secret-access-key"
R2_BUCKET="posthive-uploads"
R2_PUBLIC_URL="https://pub-xxxx.r2.dev" # your bucket's public URLSupabase Storage
- Create a Supabase project.
- In Storage, create a public bucket named media.
- Copy your project URL and service role key.
STORAGE_PROVIDER=supabase
SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_SERVICE_KEY="eyJ..."Workspaces & Teams
Overview
Every account belongs to one or more workspaces. A workspace is the billing and collaboration unit — it owns connected accounts, scheduled posts, templates, and API keys. You get a personal workspace on sign-up, and you can create additional workspaces for separate teams or projects.
Switch between workspaces using the workspace picker at the top of the sidebar. Each workspace has its own plan and subscription.
Inviting members
Workspace owners and admins can invite team members from Sidebar → Team.
- Click Invite member in the Team page header.
- Enter the email address and choose a role (Member or Admin).
- The invitee receives an email with a link to /invite?token=….
- If they don't have an account yet, the invite link guides them through registration and auto-accepts on completion.
- Accepted invites are deleted from the database — they cannot be reused.
Roles
| Role | Can post & schedule | Can manage accounts | Can invite members | Can delete workspace |
|---|---|---|---|---|
| Owner | Yes | Yes | Yes | Yes (if not last workspace) |
| Admin | Yes | Yes | Yes | No |
| Member | Yes | No | No | No |
Non-owners can leave a workspace from Settings → Danger zone → Leave workspace.
Switching workspaces
Click the workspace name at the top of the sidebar to open the workspace picker. Select any workspace you belong to — all pages (posts, accounts, billing) immediately reflect that workspace's data. You can also create a new workspace from the picker.
Billing
Plans & pricing
Posthive has four tiers. The trial is available immediately after sign-up.
| Plan | Accounts | Posts / month | Team members | Reels & Stories | API & MCP | Overrides |
|---|---|---|---|---|---|---|
| Trial | 3 | 30 | 1 | No | No | No |
| Creator | 5 | 400 | 1 | No | No | No |
| Pro | 15 | Unlimited | 2 | Yes | Yes | Yes |
| Team | 50 | Unlimited | 4 | Yes | Yes | Yes |
Billing is handled through Dodo Payments. Set ENABLE_BILLING=true and configure your Dodo API key and product IDs to activate billing.
Webhooks
Posthive listens for Dodo Payments webhook events to update subscription status in real time. Configure the webhook endpoint in your Dodo dashboard.
Webhook URL
POST https://your-api-url/billing/webhookWebhook secret
# Dodo dashboard shows: whsec_abc123...
# Set in .env:
DODO_WEBHOOK_SECRET="abc123..."Handled events
| Event | Effect |
|---|---|
| payment.succeeded | Sets the user's plan to active for the purchased tier. |
| subscription.cancelled | Marks the subscription as cancelled. Access continues until the end of the billing period. |
Outbound Webhooks
Outbound Webhooks (Zapier / n8n / Make)
Posthive fires a POSTrequest to your configured URL every time a post finishes publishing (Pro & Team plans). Use this to trigger automations in Zapier, n8n, Make, or any HTTP-capable tool.
Setup
Go to Settings → Webhook and paste your endpoint URL. Posthive will send a JSON payload to that URL after every successful (or failed) publish.
Payload
{
"event": "post.published",
"postId": "clx7k2m9e0000abc123xyz",
"status": "done",
"scheduledFor": "2026-07-05T10:00:00.000Z",
"platforms": ["bluesky", "mastodon", "linkedin"],
"text": "Your post content here"
}| Field | Type | Description |
|---|---|---|
| event | string | Always post.published |
| postId | string | The Posthive job ID |
| status | string | done (all platforms succeeded) or failed (one or more failed) |
| scheduledFor | ISO 8601 | The originally scheduled publish time |
| platforms | string[] | Platforms targeted (e.g. ["bluesky", "mastodon"]) |
| text | string | The main post text |
n8n — Webhook trigger
1. Add a Webhook node in n8n and copy the URL. 2. Paste it into Posthive Settings → Webhook. 3. Connect downstream nodes — e.g. send a Slack message, log to a Google Sheet, or notify a Notion database.
// Example: filter for only successful posts in n8n Function node
if ($json.status !== "done") return [];
return [{ json: { text: $json.text, platforms: $json.platforms } }];Zapier — Catch Hook
1. Create a new Zap → Trigger: Webhooks by Zapier → Catch Hook. 2. Copy the Zapier webhook URL and paste it into Posthive Settings → Webhook. 3. Schedule a test post in Posthive to send a sample payload. 4. Use the fields (postId, platforms, text) in your Zap actions.
Make (Integromat) — Custom Webhook
1. Add a Webhooks → Custom webhook module as the trigger. 2. Copy the URL and paste it into Posthive Settings → Webhook. 3. Run a test post — Make will auto-detect the payload structure. 4. Connect downstream modules.
Make.com Integration
Make.com Integration
Posthive has a native Make.com app. Schedule and manage social media posts directly from your Make scenarios with no webhooks or custom HTTP modules needed. Connect once with your API key and use ready-made modules for every action.
Connect your account
- Open Make.com and create a new scenario.
- Search for Posthive in the app search.
- Select any module and click Create a connection.
- In Posthive, go to Settings / API Keys and create a new API key.
- Paste the key (starts with
ph_) into Make and click Save.
API keys require a Pro or Team plan.
Available modules
| Module | Type | Description |
|---|---|---|
| List Social Accounts | Search | Returns all connected social accounts in your workspace |
| Create Post | Action | Schedule or draft a post to one or more social accounts |
| Get Post | Action | Retrieve a post by ID including per-platform status |
| List Posts | Search | Filter posts by status: pending, done, failed, or draft |
| Update Post | Action | Update content, schedule time, or images of a pending post |
| Delete Post | Action | Permanently delete a pending or draft post |
| Approve Draft | Action | Approve a draft and schedule it for publishing |
| Duplicate Post | Action | Clone an existing post as a new draft |
| List Templates | Search | Returns the design templates (pillar, category, Figma file/link) |
| Create Post from Template | Action | Create a draft for a template's pillar + category; returns the design reference |
Example scenarios
Content calendar
Trigger from Google Sheets or Notion and use Create Post to publish each row on a schedule.
Repurpose content
Use Duplicate Post to clone top-performing content and reschedule it for a new audience.
Draft approval workflow
Create Post as a draft, send a Slack notification, then Approve Draft after team review.
Content-type drafts
Run Create Post from Template with a caption to open a draft per content category, then attach the finished design in Posthive.
MCP Server
MCP Server
Posthive ships a built-in MCP (Model Context Protocol) server that exposes your scheduling queue as tools any AI agent can call — Claude, ChatGPT, Cursor, VS Code, Claude Code, Codex, OpenClaw, Hermes Agent, or your own pipeline. No API key copy-pasting required — every client uses the same bare URL and signs in via your browser. Two ways to connect:
Paste the bare MCP URL — https://api.posthive.co/mcp — into any client. It opens your browser to sign in. Zero config, no API key.
Prefer a local process over a network connection? Run `posthive-cli login`, then use the npx posthive-mcp binary instead of the URL.
For clients that don't support OAuth discovery. Embeds your API key directly in the connector URL.
Plan requirements
MCP access is available on Pro and Team plans. Self-hosters with billing disabled always have full access. The /mcp endpoint returns 403 with a clear message if your plan does not include it.
| Plan | MCP access | API keys |
|---|---|---|
| Creator | ✗ | — |
| Pro | ✓ | Unlimited |
| Team | ✓ | Unlimited |
| Self-hosted | ✓ | Unlimited |
Rate limits
The MCP endpoints are rate-limited to protect the service:
| Endpoint | Limit |
|---|---|
| POST /mcp (Bearer auth) | 60 requests / minute |
| POST /mcp/:apiKey (key in URL) | 60 requests / minute |
| POST /oauth/register | 10 registrations / minute per IP |
Sign in (recommended)
Sign in once via your browser — no API key to generate, copy, or paste. This works for both the CLI and the MCP server, since they share the same stored login:
npx posthive-cli loginThis opens your browser, and once you approve, saves your credentials to ~/.posthive/config.json. Any client running posthive-mcp locally — Cursor, VS Code, Claude Code, Codex, OpenClaw, Hermes Agent — picks this up automatically. Use npx posthive-cli whoami to confirm who's logged in, or npx posthive-cli logout to sign out.
Self-hosted Posthive: npx posthive-cli login --api-url http://localhost:3001
POSTHIVE_API_KEY=ph_... # Settings → API Keys
POSTHIVE_API_URL=https://api.posthive.coClaude Code setup
Recommended — one command, no API key:
claude mcp add --transport http posthive https://api.posthive.co/mcpThe first time Claude Code calls a Posthive tool, it opens your browser to sign in. Approve once and you're connected — same OAuth flow as the Claude and ChatGPT connectors below.
Alternative — sign in via the CLI, then run locally:
npx posthive-cli login
claude mcp add posthive -- npx posthive-mcpUseful if you'd rather sign in from the terminal up front, or already ran posthive-cli login for other tools.
Cursor, VS Code, Codex, OpenClaw, Hermes Agent
Same bare Streamable HTTP + OAuth URL as the Claude and ChatGPT connectors — no API key in any config file. The client opens your browser to sign in the first time it calls a tool.
https://api.posthive.co/mcpCursor — add to .cursor/mcp.json:
{
"mcpServers": {
"posthive": {
"url": "https://api.posthive.co/mcp"
}
}
}VS Code (GitHub Copilot Chat) — add to .vscode/mcp.json:
{
"servers": {
"posthive": {
"type": "http",
"url": "https://api.posthive.co/mcp"
}
}
}Codex — add to ~/.codex/config.toml:
[mcp_servers.posthive]
url = "https://api.posthive.co/mcp"OpenClaw — connect via MCP URL:
openclaw mcp set posthive '{"url":"https://api.posthive.co/mcp","transport":"streamable-http"}'Or install the Posthive skill directly from ClawHub (teaches OpenClaw the full CLI command set automatically):
openclaw skills install @astablackclove/posthive-mcpHermes Agent — add to ~/.hermes/config.yaml:
mcp_servers:
posthive:
url: "https://api.posthive.co/mcp"See the Agent page for step-by-step setup per client.
Hosted app connectors
Claude.ai, ChatGPT, and other hosted AI clients connect via the full OAuth 2.0 + PKCE flow. These clients are auto-registered on first use — you do not need to pre-register them via /oauth/register. Specifically, redirect URIs from claude.ai, chatgpt.com, and chat.openai.com are trusted automatically.
To disconnect, revoke the connector from the AI client's settings. This calls POST /oauth/revoke with your token, which immediately invalidates the API key that was issued for that session.
Streamable HTTP MCP endpoint — no local binary required. Claude handles OAuth automatically.
Steps:
1. Go to Claude.ai → Settings → Connectors → Add custom connector
2. Enter the Posthive MCP URL:
3. Claude.ai opens a Posthive authorization page — log in and click Allow access.
4. Claude.ai discovers all tools automatically. Revoke access anytime from Settings → API Keys.
Manual API key in URL (fallback)
Prefer the Sign in / stdio method above wherever possible. For a client that doesn't support either — embed your API key directly in the URL. No headers, no config files beyond the URL itself:
https://api.posthive.co/mcp/ph_your_api_key_herePaste that as the MCP server URL in your agent settings. The agent discovers all 10 tools automatically.
Cursor: Settings → MCP → Add server → Type: HTTP → URL: paste above.
Claude Code:
claude mcp add --transport http posthive https://api.posthive.co/mcp/ph_your_api_key_hereCLI for shell agents (OpenClaw, custom pipelines)
Not every agent speaks MCP. For agents that run shell commands — OpenClaw, custom automation, or scripts — install posthive-cli, a thin command-line wrapper over the same public API:
npx posthive-cli loginOpens your browser to sign in — no API key to copy or paste. Credentials are stored in ~/.posthive/config.json, shared automatically with posthive-mcp too. Use posthive whoami to check who's logged in, or posthive logout to sign out.
npx posthive-cli accounts:list
npx posthive-cli posts:create --content "Hello" --accounts acc_1,acc_2
npx posthive-cli posts:list --status draftEvery command outputs structured JSON. The package ships a bundled skills/posthive/SKILL.md that teaches agents the full command set, platform character limits, and the draft-first workflow — so capable agents can self-discover usage without extra prompting.
Available tools
All tools are available on both transports. The full set:
| Tool | Description | Key params |
|---|---|---|
| list_accounts | List all connected social accounts and their IDs | — |
| create_post | Create a post (draft by default — nothing publishes without approval) | content, account_ids, schedule_directly, scheduled_time, per_account, first_comment, dry_run |
| get_post | Get full details + per-platform publish status for a single post | post_id |
| list_scheduled_posts | List queue — filter by status, limit results | status, limit |
| approve_draft | Promote a draft to scheduled — closes the agent → review → publish loop | post_id, scheduled_time |
| update_post | Update content, time, or per-account overrides on pending/draft posts | post_id, content, scheduled_time, first_comment, per_account |
| duplicate_post | Clone any post as a new draft | post_id |
| delete_post | Delete a pending or draft post | post_id |
| list_templates | List design templates (one reference design per content category) | — |
| create_from_template | Draft a post for a template's pillar + category; returns the design file/link as a reference (scheduling needs content_override) | template_id, account_ids, content_override, first_comment_override, schedule_directly, scheduled_time |
Testing without publishing
Pass dry_run: true to create_post to run the full pipeline — validation, target resolution, content parsing — without making any real API calls to social platforms. Useful for verifying your agent's post logic before going live.
Media, video types & platform formats
The create_post tool supports images, videos, and platform-specific formats via three optional fields:
| Field | Values | Platform |
|---|---|---|
| media_urls | Array of uploaded file URLs | All platforms |
| media_type | "post" | "reel" | "story" | Instagram only |
| youtube_type | "short" | "video" | YouTube only |
- Upload the file via POST /api/v1/upload (multipart, see API Reference).
- Copy the url from the response.
- Pass it in media_urls when calling create_post.
Example prompts
Once connected, use natural language — the agent picks the right tools automatically:
“List my accounts, then save a draft for Bluesky, Threads, and LinkedIn with platform-native copy — punchy hook for Threads (under 280 chars), thread format for Bluesky, long-form for LinkedIn. I'll review and schedule them myself. Post topic: "We just shipped Posthive MCP — your AI agent can now manage your entire content queue."”
“List my current drafts and show me what's waiting for review. For any drafts that look ready, approve them and schedule them for tomorrow at 9am IST.”
“Find the Case Study design template, draft a post for it to all my accounts with a caption for today's client story, and send me the Figma link so I can finish the design.”
“Show me everything scheduled for this week. Reschedule any posts that land on Saturday or Sunday to the following Monday at 10am. Delete any drafts older than 7 days.”
API Reference
Authentication
The Posthive REST API lets you schedule posts and manage accounts programmatically — useful for AI agents, automation scripts, and custom integrations. API access is available on Pro and Team plans. When self-hosting with ENABLE_BILLING unset, all users have access.
Base URL
https://your-api-url/api/v1Creating an API key
Go to Settings → API Keys, enter a label (e.g. "My Claude agent"), and click Create key. The full key is shown only once — copy it immediately. Keys are prefixed with ph_ for easy identification.
Sending requests
Pass the key in the Authorization header of every request:
Authorization: Bearer ph_<your-key>Response format
All responses are JSON. Successful responses return the resource directly. Errors return an object with an error string field.
# Success
{ "accounts": [ ... ] }
# Error
{ "error": "One or more accountIds are invalid" }GET /accounts
Returns all social accounts connected to your Posthive account. Use the returned id values as accountIds when creating posts.
Request
GET /api/v1/accounts
Authorization: Bearer ph_<key>Response
{
"accounts": [
{
"id": "cmr289odh000110a5s6rdfmzn",
"platform": "bluesky", // bluesky | threads | instagram | linkedin
"displayName": "you.bsky.social", // | mastodon | youtube | facebook | pinterest
"avatarUrl": "https://cdn.bsky.app/...",
"createdAt": "2026-07-01T15:25:53.956Z"
}
]
}Example
curl https://your-api-url/api/v1/accounts \
-H "Authorization: Bearer ph_<key>"POST /posts
Schedule a post to one or more connected accounts. The post is queued and published automatically at scheduledFor.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| content | string | Yes | Post text. Used for all platforms unless overridden via perAccount. |
| accountIds | string[] | Yes | One or more account IDs from GET /accounts. All must belong to your account. |
| scheduledFor | string (ISO 8601) | Yes | Future UTC datetime to publish. |
| commentText | string | No | First comment/reply posted immediately after the main post. Override per-platform via perAccount. |
| images | string[] | No | Image URLs from POST /upload. Supported on Bluesky, Mastodon, Threads, Instagram, LinkedIn, Facebook, Telegram. |
| altTexts | string[] | No | Alt text for each image, matched by index. Improves accessibility. |
| mediaType | "post" | "reel" | "story" | No | Instagram/Facebook media format. Defaults to "post". |
| youtubeType | "short" | "video" | No | YouTube upload format. Defaults to "short". |
| dryRun | boolean | No | If true, runs the full scheduling pipeline without making any real platform API calls. Useful for testing. |
| perAccount | object | No | Per-platform content overrides keyed by account ID. See below. |
Per-platform overrides
Use perAccount to post different text or a different first comment to specific accounts. This is the recommended approach when targeting platforms with different character limits (e.g. Bluesky 300 chars vs LinkedIn 3,000 chars).
| Platform | Char limit |
|---|---|
| Bluesky | 300 |
| Threads | 500 |
| Mastodon | 500 |
| Pixelfed | 2,200 |
| 2,200 | |
| 63,206 | |
| 3,000 | |
| YouTube | 5,000 (description) |
| 100 (title) / 500 (description) | |
| Telegram | 4,096 |
Response — 201 Created
{
"id": "cmr39jppf0003iiwnt643zr91",
"scheduledFor": "2026-07-03T10:00:00.000Z",
"status": "pending",
"targets": [
{
"id": "cmr39jppf0005iiwn0gkg3mpq",
"accountId": "cmr289odh000110a5s6rdfmzn",
"status": "pending"
}
]
}Example — multi-platform with per-account overrides
curl -X POST https://your-api-url/api/v1/posts \
-H "Authorization: Bearer ph_<key>" \
-H "Content-Type: application/json" \
-d '{
"content": "Short version for Bluesky (under 300 chars) 🚀",
"accountIds": ["<bluesky-id>", "<linkedin-id>"],
"scheduledFor": "2026-07-03T10:00:00.000Z",
"perAccount": {
"<linkedin-id>": {
"text": "Longer LinkedIn version with more context and hashtags. #buildinpublic #saas",
"commentText": "Happy to answer questions below!"
}
}
}'Example — dry run (test without posting)
curl -X POST https://your-api-url/api/v1/posts \
-H "Authorization: Bearer ph_<key>" \
-H "Content-Type: application/json" \
-d '{
"content": "Test post — will not actually publish",
"accountIds": ["<account-id>"],
"scheduledFor": "2026-07-03T10:00:00.000Z",
"dryRun": true
}'GET /posts
Returns your scheduled and completed posts in reverse chronological order (newest first). Supports cursor-based pagination.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
| status | string | — | Filter by status: pending, running, done, failed. Omit to return all. |
| limit | number | 20 | Results per page. Maximum 100. |
| cursor | string | — | Post id of the last item from the previous page. Omit for the first page. |
Response
{
"posts": [
{
"id": "cmr39jppf0003iiwnt643zr91",
"scheduledFor": "2026-07-03T10:00:00.000Z",
"status": "done",
"content": "Hello from the Posthive API! 🚀",
"targets": [
{ "id": "...", "accountId": "...", "status": "done", "error": null }
]
}
],
"nextCursor": "cmr39jppf0003iiwnt643zr91" // null on last page
}Paginating
# Page 1
GET /api/v1/posts?limit=20
# Page 2 — pass the last post id as cursor
GET /api/v1/posts?limit=20&cursor=cmr39jppf0003iiwnt643zr91GET /posts/:id
Returns a single post by ID, including full per-platform target details and any error messages.
Response
{
"id": "cmr39jppf0003iiwnt643zr91",
"scheduledFor": "2026-07-03T10:00:00.000Z",
"status": "done",
"content": "Hello from the Posthive API! 🚀",
"commentText": null,
"targets": [
{
"id": "cmr39jppf0005iiwn0gkg3mpq",
"accountId": "cmr289odh000110a5s6rdfmzn",
"status": "done",
"error": null,
"platformPostId": "at://did:plc:abc.../app.bsky.feed.post/123"
}
],
"createdAt": "2026-07-02T08:00:00.000Z",
"updatedAt": "2026-07-03T10:00:05.000Z"
}Target statuses
| Status | Meaning |
|---|---|
| pending | Waiting to be published |
| running | Currently being published |
| post_done | Main post published, first comment pending |
| done | Fully published (post + comment if set) |
| post_failed | Publishing failed — see error field |
| comment_failed | Post published but first comment failed |
Example
curl https://your-api-url/api/v1/posts/cmr39jppf0003iiwnt643zr91 \
-H "Authorization: Bearer ph_<key>"PATCH /posts/:id
Update a pending post — reschedule it, change the content, swap accounts, or update the first comment. All fields are optional; only send what you want to change.
Request body
All fields are optional — only send what you want to change.
| Field | Type | Description |
|---|---|---|
| scheduledFor | string (ISO 8601) | New publish time. Must be in the future. |
| content | string | Replacement post text (default for all platforms). |
| commentText | string | Replacement first comment. Pass "" to remove. |
| accountIds | string[] | Replace target accounts entirely. Must be non-empty. |
| images | string[] | Replace attached images with new upload URLs. |
| altTexts | string[] | Alt text for each image, matched by index. |
| mediaType | "post" | "reel" | "story" | Instagram/Facebook media format. |
| youtubeType | "short" | "video" | YouTube upload format. |
| perAccount | object | Per-platform text/comment overrides keyed by account ID. |
Response — 200 OK
{
"id": "cmr39jppf...",
"scheduledFor": "2026-07-04T10:00:00.000Z",
"status": "pending",
"content": "Updated post text",
"commentText": null
}Example — reschedule only
curl -X PATCH https://your-api-url/api/v1/posts/cmr39jppf... \
-H "Authorization: Bearer ph_<key>" \
-H "Content-Type: application/json" \
-d '{ "scheduledFor": "2026-07-04T10:00:00.000Z" }'DELETE /posts/:id
Cancels and permanently deletes a pending post. Posts with status running, done, or failed cannot be deleted via the API.
Response — 200 OK
{ "ok": true }Example
curl -X DELETE https://your-api-url/api/v1/posts/cmr39jppf0003iiwnt643zr91 \
-H "Authorization: Bearer ph_<key>"POST /upload
Upload an image or video file and get back a URL you can pass in the images field of POST /posts. Send as multipart/form-data with the file in a field named file.
Supported types
| Type | Formats | Max size |
|---|---|---|
| Image | image/jpeg, image/png, image/gif, image/webp | 10 MB |
| Video | video/mp4, video/quicktime | 100 MB |
Response — 201 Created
{
"url": "https://your-api-url/uploads/abc123.jpg",
"type": "image" // "image" | "video"
}Example
curl -X POST https://your-api-url/api/v1/upload \
-H "Authorization: Bearer ph_<key>" \
-F "file=@/path/to/photo.jpg"
# Then use the returned URL in a post
curl -X POST https://your-api-url/api/v1/posts \
-H "Authorization: Bearer ph_<key>" \
-H "Content-Type: application/json" \
-d '{
"content": "Check out this photo!",
"accountIds": ["cmr289odh..."],
"scheduledFor": "2026-07-04T10:00:00.000Z",
"images": ["https://your-api-url/uploads/abc123.jpg"]
}'Error codes
All errors return a JSON body with an error field describing what went wrong.
| HTTP status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid field. Check the error message. |
| 401 | Missing, invalid, or revoked API key. |
| 403 | Your plan does not include API access, or you have reached your key limit. |
| 404 | Resource not found or does not belong to your account. |
| 500 | Unexpected server error. Try again or contact support. |