Getting Started

Posthive Documentation

Schedule posts to multiple platforms from one interface with built-in MCP support for AI agents.

Don't want to manage infrastructure?Try posthive.co free no setup needed →

Quick start

Get Posthive running locally in under five minutes.

1. Clone the repository

git clone https://github.com/AstaBlackClove/posthive.git cd posthive

2. Copy environment files

cp apps/api/.env.example apps/api/.env # Edit apps/api/.env and fill in the required values

3. Run database migrations

cd apps/api pnpm db:migrate

4. Start the dev server

# From the repo root pnpm dev

This 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 install

The 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.

VariableRequiredDescription
DATABASE_URLYesSQLite path (file:./dev.db) for dev, or a Postgres connection string for prod.
ENCRYPTION_KEYYes64-char hex string. Used for AES-256-GCM encryption of stored credentials. Never change after data is written.
REDIS_URLYesRedis connection string (e.g. rediss://...) for BullMQ job queue.
JWT_ACCESS_SECRETYes64-char hex. Signs short-lived access tokens.
JWT_REFRESH_SECRETYes64-char hex. Signs long-lived refresh tokens.
WEB_URLYesURL of the web app. Use http://localhost:3000 in dev so auth cookies stay stable across tunnel restarts.
PUBLIC_API_URLMeta onlyPublic HTTPS URL of the API. Meta fetches images from this URL, so it must be reachable from the internet.
ENABLE_BILLINGNoSet to true to enable Dodo Payments billing. Defaults to off for self-hosters.
AUTH_PROVIDERNolocal (default) or supabase. Switches the auth backend.
GOOGLE_CLIENT_IDNoGoogle OAuth client ID. Enables "Sign in with Google" on the login and register pages.
GOOGLE_CLIENT_SECRETNoGoogle OAuth client secret. Required alongside GOOGLE_CLIENT_ID.
GOOGLE_REDIRECT_URINoCallback URL registered in Google Cloud Console. Defaults to http://localhost:3001/auth/google/callback.
ENABLE_ANALYTICSNoSet to true to enable visitor session + event tracking. Off by default self-hosters collect no data.
ADMIN_EMAILNoYour email address. Only this account can access /admin.
ADMIN_PINNoSecond-factor PIN for /admin. Validated server-side never exposed to the browser. Set in API env only.
ANTHROPIC_API_KEYNoAnthropic 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_KEYTikTok onlyTikTok app client key from developers.tiktok.com.
TIKTOK_CLIENT_SECRETTikTok onlyTikTok app client secret.
TIKTOK_REDIRECT_URITikTok onlyOAuth callback URL. Must match exactly what is registered in the TikTok developer portal.
Warning: ENCRYPTION_KEY must never be changed after connected accounts have been saved. Changing it makes all stored credentials permanently unreadable.

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.

Google Sign-In is optional. If you skip this, users can only register and log in with email + password.

1. Create a Google OAuth client

  1. Go to console.cloud.google.com and open (or create) a project.
  2. Navigate to APIs & Services → OAuth consent screen. Set User Type to External, fill in the app name and your email, then save.
  3. Go to APIs & Services → Credentials → Create Credentials → OAuth client ID.
  4. 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).
  5. Copy the Client ID and Client Secret.
If you already have a Google Cloud project for YouTube, you can reuse the same OAuth client — just add the Google Sign-In redirect URI alongside the existing YouTube one.

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

  1. Go to bsky.app → Settings → Privacy and Security → App Passwords.
  2. Create a new app password and copy it.
  3. Open the Accounts page in Posthive, click Connect Bluesky.
  4. 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

  1. Go to developers.facebook.com and create an app.
  2. Add the Threads API use case to your app.
  3. Add a redirect URI matching THREADS_REDIRECT_URI in your env must be a public HTTPS URL.
  4. Copy your App ID and App Secret into THREADS_APP_ID and THREADS_APP_SECRET.
  5. 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

Instagram publishing requires a Professional (Business or Creator) account linked to a Facebook Page, and a Meta Developer app with the Instagram product enabled.

Important: PUBLIC_API_URL must be a public HTTPS URL. Meta fetches your uploaded images directly from the API server when creating carousel containers a localhost URL will not work.

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

  1. Create a Meta Developer app and add the Instagram product.
  2. Set your redirect URI to INSTAGRAM_REDIRECT_URI (must be public HTTPS).
  3. Fill in INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET, and PUBLIC_API_URL.
  4. Click Connect Instagram on the Accounts page.

LinkedIn

LinkedIn uses OAuth 2.0 via the LinkedIn Developer platform.

Note: Image and video uploads require elevated API access (Marketing Developer Platform approval). Without it, Posthive will publish text-only posts to LinkedIn.

Setup

  1. Create an app at developer.linkedin.com.
  2. Add the Share on LinkedIn and Sign In with LinkedIn using OpenID Connect products.
  3. Add your callback URL under AuthAuthorized redirect URLs.
  4. Set LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET in your env.
  5. 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

  1. Log in to your Mastodon instance (e.g. mastodon.social).
  2. Go to Settings → Development → New application.
  3. Give it a name, set the redirect URI to your Posthive callback URL, and enable the write:statuses and write:media scopes.
  4. Copy the Client key and Client secret.
  5. 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

  1. Create a free account on any Pixelfed instance (e.g. pixelfed.social or pixelfed.uno).
  2. 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.
  3. 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

  1. Create a project at console.cloud.google.com and enable the YouTube Data API v3.
  2. 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.
  3. Create an OAuth client (Web application) and copy the Client ID and Secret into YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET.
  4. Click Connect YouTube on the Accounts page and authorize.
Important: Google requires OAuth redirect domains to be owned and verified shared tunnel domains (devtunnels.ms, ngrok, etc.) are rejected outright with Error 403: access_denied. Use http://localhost:<API_PORT>/auth/youtube/callback for YOUTUBE_REDIRECT_URI instead Google exempts localhost from domain verification. This means connecting YouTube only works from a browser on the same machine as your API server (everything else in Posthive works fine over a tunnel).

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.

While the Google app stays in "Testing" publishing status, refresh tokens expire after 7 days regardless of activity scheduled YouTube posts will start failing until you manually reconnect. Submit the app for Google verification to remove this limit and the 100-test-user cap.

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

  1. 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.
  2. Under Facebook Login for Business → Settings, add your callback URL as a valid OAuth redirect URI: https://your-domain/auth/facebook/callback.
  3. 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).
  4. Create a Facebook Page at facebook.com/pages/create if you don't have one.
  5. Click Connect Facebook Page on the Accounts page and authorise all Pages you admin will be connected automatically.
First comment support requires the additional pages_manage_engagement permission, which needs Meta app review approval. Until approved, posts publish successfully but first comments are skipped.

Pinterest

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.

Trial access only: Pinterest Standard access (required for production posting) requires approval from Pinterest. Until approved, Posthive connects to the Pinterest sandbox environment. The connect button on the Accounts page shows Pending Approval and is disabled until Standard access is granted. See below for what to do once approved.

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

VariableRequiredDescription
PINTEREST_CLIENT_IDYesOAuth app Client ID from the Pinterest developer portal.
PINTEREST_CLIENT_SECRETYesOAuth app Client Secret.
PINTEREST_REDIRECT_URIYesMust match the redirect URI registered in the Pinterest app settings.
PINTEREST_SANDBOXNoSet to true to use the Pinterest sandbox API. Required for Trial access apps.
PINTEREST_SANDBOX_TOKENNoManually 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)

  1. Remove PINTEREST_SANDBOX=true and PINTEREST_SANDBOX_TOKEN from your env.
  2. The Pinterest connect button on the Accounts page will be re-enabled in the next release.
  3. Users connect via the normal OAuth flow — no manual token required.
Image required: The Schedule button is disabled when Pinterest is selected and no image has been attached. Add at least one image before scheduling a Pinterest post.

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

  1. Open Telegram and message @BotFather → send /newbot → follow the prompts. BotFather gives you a bot token (looks like 123456789:ABCdef...).
  2. Create a Telegram channel — public or private.
  3. Add your bot to the channel as an Administrator: open the channel → Administrators → Add Administrator → search your bot → enable Post Messages → Done.
  4. Go to Accounts in Posthive and click Connect Telegram Channel.
  5. 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.
No environment variables needed. Unlike OAuth platforms, Telegram requires no server-side app credentials. Each user provides their own bot token which is stored encrypted per-user. One bot can serve multiple channels — connect each channel separately on the Accounts page.

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

  1. Go to Accounts in Posthive and click Connect Nostr.
  2. Paste your nsec1... private key — or click Generate a new keypair to create a fresh one.
  3. Your public key (npub) and profile photo are fetched automatically from relays.
No environment variables needed. Nostr uses keypairs — there is no server-side app registration or OAuth flow. Your nsec is stored AES-256-GCM encrypted per-user, never logged or exposed.

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

  1. Go to discord.com/developersNew Application → name it Posthive.
  2. Go to the Bot tab → click Add Bot → copy the bot token.
  3. Go to OAuth2 → General → copy Client ID and Client Secret. Add your redirect URI.
  4. 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"
  1. Go to Accounts in Posthive → click Connect Discord.
  2. Authorise the bot on your Discord server, then pick a channel in the Posthive channel picker.
Posthive auto-creates a webhook for the selected channel. Messages post under the Posthive name. Discord shows an APP label on all programmatic posts — this is a Discord platform requirement that applies to all scheduling tools.

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.

  1. Go to tumblr.com/oauth/appsRegister application. No review required.
  2. Set Default callback URL to your production API domain + /auth/tumblr/callback.
  3. 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"
  1. Go to Accounts in Posthive → click Connect Tumblr → approve the OAuth prompt.
Tumblr app registration is instant — no business verification, no waitlist. Your consumer key is available immediately after registering.

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.

No app registration required. You just need a Lemmy account on any instance and a community to post to.

Connect

  1. Go to Accounts in Posthive → click Connect Lemmy.
  2. Enter your instance URL (e.g. https://lemmy.world), username, password, and the community name (e.g. technology — no ! prefix needed).
  3. 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.
Lemmy credentials are stored encrypted. If you change your Lemmy password, reconnect the account in Posthive.

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.

  1. Go to developers.tiktok.com → create an app.
  2. Add products: Login Kit and Content Posting API. Enable Direct Post under Content Posting API.
  3. Set the redirect URI to https://your-domain.com/auth/tiktok/callback.
  4. 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"
  1. Go to Accounts in Posthive → click Connect TikTok.
TikTok requires app review before posts can be made to public accounts. During review, your TikTok account must be set to Private and posts will be visible only to you. Once TikTok approves the app, posts go public and the account can be set back to public.

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

  1. Select one or more connected accounts from the account picker at the top.
  2. Write your post in the text area. The character counter updates per-platform.
  3. Optionally attach images or video using the media button.
  4. Pick a scheduled time using the date-time picker. You can also post immediately.
  5. Click Schedule. The post is enqueued and will fire at the chosen time.

Post templates

Templates let you save and reuse post content from the Compose page. Useful for recurring formats like weekly updates, product announcements, or thread starters.

Saving a template

  1. Write your post in the Compose page.
  2. Click + Save in the POST section header.
  3. Enter a name and press Enter or click Save. Template names must be unique.

Loading a template

  1. Click Templates in the POST section header to open the dropdown.
  2. Click any template name — the text and first comment are instantly populated.

Templates save: post text, first comment, and YouTube title/description (if YouTube fields were filled). Templates are not shown when only YouTube is selected.

Deleting a template

Hover over a template in the dropdown and click the that appears on the right. A confirmation dialog will ask you to confirm before deleting.

Bulk CSV scheduling

Bulk scheduling lets you upload a CSV file to schedule tens or hundreds of posts at once. Available from both the Posts page (Bulk button in the top bar) and the Compose page (Bulk CSV button next to Schedule).

CSV format

The CSV must have a header row with these columns (in order):

scheduled_for,text,accounts,comment,image_urls
  • scheduled_for — date and time in YYYY-MM-DD HH:MM format or ISO 8601. Must be in the future.
  • text — post body text (required).
  • accounts — which platforms to post to (see below).
  • comment — first comment text (optional, leave blank).
  • image_urls — public image URLs separated by ; (optional, up to 4).

Accounts column syntax

  • all — post to all connected accounts (excluding YouTube).
  • bluesky|mastodon — post to specific platforms, separated by |.
  • !instagram — post to all platforms except Instagram (and YouTube).
  • all|!instagram|!linkedin — all except Instagram and LinkedIn.

Supported platform names: bluesky, threads, instagram, linkedin, mastodon, facebook, telegram. YouTube is not supported in bulk scheduling — it requires a video file. Use the Compose page for YouTube posts.

Instagram rows must include at least one URL in image_urls. Rows missing an image for Instagram will show an error in the preview and be skipped.

Example CSV

scheduled_for,text,accounts,comment,image_urls 2026-08-01 09:00,Good morning 🌅,all,, 2026-08-02 14:30,Check the blog post,bluesky|mastodon,Link in first comment, 2026-08-03 18:00,LinkedIn update,linkedin,,https://example.com/image.jpg 2026-08-04 10:00,Skip Instagram today,!instagram,, 2026-08-05 12:00,Two images 🖼️,bluesky|threads,,https://img1.jpg;https://img2.jpg

Preview and scheduling

  1. Paste your CSV or upload a .csv file.
  2. Click Preview — each row is validated and shown in a table with its status (✓ Ready or ✕ error).
  3. Review the results. Error rows are skipped automatically.
  4. Click Schedule N posts to enqueue all valid rows. Posts are scheduled one at a time with a progress bar.

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=supabase and configure your bucket.

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 posthive

Step 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 .env

Step 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'))"
Critical: ENCRYPTION_KEY encrypts all stored OAuth tokens. Changing it after accounts are connected makes every connected account permanently unusable. Back it up somewhere safe.

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.com

Everything 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/callback

Bluesky, 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 --build

First build takes 2–5 minutes. Once done, open your domain and register your account. Billing is disabled by default — all features are unlocked.

ContainerDefault portPurpose
web3000Next.js frontend
api3001Fastify backend + BullMQ worker
db5432 (internal)Postgres 16
redis6379 (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.com

Rebuild after changing URLs:

docker compose up -d --build

Caddy (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 this

For production, use Supabase Storage instead — set STORAGE_PROVIDER=supabase and the SUPABASE_* env vars. Files are stored off-server and survive rebuilds automatically.

Updating

git pull docker compose up -d --build

Database 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.js

Database

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:migrate
Warning: ENCRYPTION_KEY must never change after accounts are saved. All stored OAuth tokens and app passwords are encrypted with this key changing it renders all connected accounts permanently unusable.

Redis

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) or in Supabase Storage (prod). Switch with the STORAGE_PROVIDER env variable.

Local storage (default)

STORAGE_PROVIDER=local

Files are written to apps/api/uploads/. Not recommended for production files are lost on redeploy.

Supabase Storage

  1. Create a Supabase project.
  2. In Storage, create a public bucket named media.
  3. 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.

  1. Click Invite member in the Team page header.
  2. Enter the email address and choose a role (Member or Admin).
  3. The invitee receives an email with a link to /invite?token=….
  4. If they don't have an account yet, the invite link guides them through registration and auto-accepts on completion.
  5. Accepted invites are deleted from the database — they cannot be reused.
Invite links expire after 7 days. Resend from the pending invites list if needed.

Roles

RoleCan post & scheduleCan manage accountsCan invite membersCan delete workspace
OwnerYesYesYesYes (if not last workspace)
AdminYesYesYesNo
MemberYesNoNoNo

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.

Each workspace has its own billing plan. Creating a second workspace starts a new trial for that workspace. Upgrade each workspace independently.

Billing

Plans & pricing

Posthive has four tiers. The trial is available immediately after sign-up.

PlanAccountsPosts / monthTeam membersReels & StoriesAPI & MCPOverrides
Trial3301NoNoNo
Creator54001NoNoNo
Pro15Unlimited2YesYesYes
Team50Unlimited4YesYesYes

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/webhook

Webhook secret

Important: Dodo Payments webhook secrets are prefixed with whsec_. Strip this prefix before setting DODO_WEBHOOK_SECRET the verification code base64-decodes the raw secret and will fail if the prefix is included.
# Dodo dashboard shows: whsec_abc123... # Set in .env: DODO_WEBHOOK_SECRET="abc123..."

Handled events

EventEffect
payment.succeededSets the user's plan to active for the purchased tier.
subscription.cancelledMarks 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" }
FieldTypeDescription
eventstringAlways post.published
postIdstringThe Posthive job ID
statusstringdone (all platforms succeeded) or failed (one or more failed)
scheduledForISO 8601The originally scheduled publish time
platformsstring[]Platforms targeted (e.g. ["bluesky", "mastodon"])
textstringThe main post text
Delivery is best-effort with a 10-second timeout. Posthive does not retry failed webhook deliveries — if your endpoint is down, the event is lost. Build your handler to be idempotent.

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

  1. Open Make.com and create a new scenario.
  2. Search for Posthive in the app search.
  3. Select any module and click Create a connection.
  4. In Posthive, go to Settings / API Keys and create a new API key.
  5. Paste the key (starts with ph_) into Make and click Save.

API keys require a Pro or Team plan.

Available modules

ModuleTypeDescription
List Social AccountsSearchReturns all connected social accounts in your workspace
Create PostActionSchedule or draft a post to one or more social accounts
Get PostActionRetrieve a post by ID including per-platform status
List PostsSearchFilter posts by status: pending, done, failed, or draft
Update PostActionUpdate content, schedule time, or images of a pending post
Delete PostActionPermanently delete a pending or draft post
Approve DraftActionApprove a draft and schedule it for publishing
Duplicate PostActionClone an existing post as a new draft
List TemplatesSearchReturns all saved post templates
Create Post from TemplateActionCreate a new post using a saved template

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.

Template publishing

Run Create Post from Template on a cron schedule to publish recurring content automatically.

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:

OAuth connectorall clients

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.

stdio + loginalternative

Prefer a local process over a network connection? Run `posthive-cli login`, then use the npx posthive-mcp binary instead of the URL.

Key in URLfallback

For clients that don't support OAuth discovery. Embeds your API key directly in the connector URL.

Human-in-the-loop by default. Agent-created posts are saved as drafts and require your review before anything publishes. Open Posthive → Posts to approve, edit, or schedule them. Use schedule_directly: true only when you explicitly want to skip the review step.

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.

PlanMCP accessAPI keys
Creator
ProUnlimited
TeamUnlimited
Self-hostedUnlimited

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 login

This 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

Prefer a manually generated API key instead (CI, scripts, or without installing the CLI)? Set POSTHIVE_API_KEY as an env var — it always overrides the stored login when present.
POSTHIVE_API_KEY=ph_... # Settings → API Keys POSTHIVE_API_URL=https://api.posthive.co

Claude Code setup

Recommended — one command, no API key:

claude mcp add --transport http posthive https://api.posthive.co/mcp

The 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-mcp

Useful if you'd rather sign in from the terminal up front, or already ran posthive-cli login for other tools.

How to check it's connected: run /mcp inside the Claude Code terminal. You should see posthive listed with a connected status — select it to see all 10 available tools. Then try a prompt like "list my Posthive accounts" to confirm it actually calls the tool and returns real data.

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/mcp

Cursor — 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-mcp

Hermes 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.

Prefer running the MCP server locally instead of over the network? Sign in once with npx posthive-cli login, then use { "command": "npx", "args": ["posthive-mcp"] } (add "type": "stdio" where the client requires it) instead of the URL above — still no API key needed, since posthive-mcp reads the same stored login.

Claude connector (no install needed)

The Posthive API exposes a 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 your Posthive API URL as the connector URL:

https://api.posthive.co/mcp

3. Claude.ai will open a Posthive authorization page — log in and click Allow access.

4. Claude.ai discovers all tools automatically. You can revoke access anytime from Settings → API Keys.

Self-hosters: replace https://api.posthive.co with your own API URL. The OAuth flow and /mcp endpoint are included in every deployment.

ChatGPT connector

Same Streamable HTTP + OAuth endpoint as the Claude connector above — confirmed working with ChatGPT's Developer Mode apps.

Steps:

1. Go to ChatGPT → Settings → Apps → Advanced settings and turn on Developer mode (one-time; may require your workspace admin to allow it first).

2. Go to Settings → Apps → Add app / Add custom connector and enter the same URL:

https://api.posthive.co/mcp

3. ChatGPT opens a Posthive authorization page — log in and click Allow access.

4. Try a prompt like "@posthive list my connected accounts" to confirm it's working.

Developer Mode custom connectors are private to your account — no app review or OpenAI approval needed. That's separate from submitting Posthive to the public ChatGPT App Directory, which does require identity verification and review.

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_here

Paste 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_here
Keep this URL private — it contains your API key. Revoke and regenerate from Settings → API Keys if it leaks.

CLI 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 login

Opens 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 draft

Every 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.

Prefer env vars for CI or scripts? POSTHIVE_API_KEY and POSTHIVE_API_URL always override the stored login when set.
Same safety model as MCP: posts default to drafts. Pass --schedule explicitly to schedule directly instead of saving as a draft.

Available tools

All tools are available on both transports. The full set:

ToolDescriptionKey params
list_accountsList all connected social accounts and their IDs
create_postCreate a post (draft by default — nothing publishes without approval)content, account_ids, schedule_directly, scheduled_time, per_account, first_comment
get_postGet full details + per-platform publish status for a single postpost_id
list_scheduled_postsList queue — filter by status, limit resultsstatus, limit
approve_draftPromote a draft to scheduled — closes the agent → review → publish looppost_id, scheduled_time
update_postUpdate content, time, or per-account overrides on pending/draft postspost_id, content, scheduled_time, first_comment, per_account
duplicate_postClone any post as a new draftpost_id
delete_postDelete a pending or draft postpost_id
list_templatesList saved post templates
create_from_templateDraft or schedule a post from a saved template (with optional overrides)template_id, account_ids, content_override, schedule_directly, scheduled_time

Media, video types & platform formats

The create_post tool supports images, videos, and platform-specific formats via three optional fields:

FieldValuesPlatform
media_urlsArray of uploaded file URLsAll platforms
media_type"post" | "reel" | "story"Instagram only
youtube_type"short" | "video"YouTube only
Media upload limitation. MCP tools cannot transfer binary files — you cannot upload an image or video directly through an agent prompt. To attach media to an MCP-created post:
  1. Upload the file via POST /api/v1/upload (multipart, see API Reference).
  2. Copy the url from the response.
  3. Pass it in media_urls when calling create_post.
A media library UI for uploading assets and copying URLs without writing code is on the roadmap.

Example prompts

Once connected, use natural language — the agent picks the right tools automatically:

EXAMPLE 1
Draft from scratch

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."

EXAMPLE 2
Review & approve

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.

EXAMPLE 3
Template workflow

Load my "Product launch" template, customise the copy for today's announcement, and save a draft to all my accounts. Add the product URL in the first comment.

EXAMPLE 4
Full queue management

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/v1

Creating 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>
Keep keys secret. Do not commit them to source control or expose them in client-side code. Revoke any compromised key from Settings → API Keys immediately — revocation takes effect within seconds.

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

FieldTypeRequiredDescription
contentstringYesPost text. Used for all platforms unless overridden via perAccount.
accountIdsstring[]YesOne or more account IDs from GET /accounts. All must belong to your account.
scheduledForstring (ISO 8601)YesFuture UTC datetime to publish.
commentTextstringNoFirst comment/reply posted immediately after the main post. Override per-platform via perAccount.
imagesstring[]NoImage URLs from POST /upload. Supported on Bluesky, Mastodon, Threads, Instagram, LinkedIn, Facebook, Telegram.
altTextsstring[]NoAlt text for each image, matched by index. Improves accessibility.
mediaType"post" | "reel" | "story"NoInstagram/Facebook media format. Defaults to "post".
youtubeType"short" | "video"NoYouTube upload format. Defaults to "short".
dryRunbooleanNoIf true, runs the full scheduling pipeline without making any real platform API calls. Useful for testing.
perAccountobjectNoPer-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).

PlatformChar limit
Bluesky300
Threads500
Mastodon500
Pixelfed2,200
Instagram2,200
Facebook63,206
LinkedIn3,000
YouTube5,000 (description)
Pinterest100 (title) / 500 (description)
Telegram4,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

ParamTypeDefaultDescription
statusstringFilter by status: pending, running, done, failed. Omit to return all.
limitnumber20Results per page. Maximum 100.
cursorstringPost 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=cmr39jppf0003iiwnt643zr91

GET /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

StatusMeaning
pendingWaiting to be published
runningCurrently being published
post_doneMain post published, first comment pending
doneFully published (post + comment if set)
post_failedPublishing failed — see error field
comment_failedPost 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.

FieldTypeDescription
scheduledForstring (ISO 8601)New publish time. Must be in the future.
contentstringReplacement post text (default for all platforms).
commentTextstringReplacement first comment. Pass "" to remove.
accountIdsstring[]Replace target accounts entirely. Must be non-empty.
imagesstring[]Replace attached images with new upload URLs.
altTextsstring[]Alt text for each image, matched by index.
mediaType"post" | "reel" | "story"Instagram/Facebook media format.
youtubeType"short" | "video"YouTube upload format.
perAccountobjectPer-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

TypeFormatsMax size
Imageimage/jpeg, image/png, image/gif, image/webp10 MB
Videovideo/mp4, video/quicktime100 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 statusMeaning
400Bad request — missing or invalid field. Check the error message.
401Missing, invalid, or revoked API key.
403Your plan does not include API access, or you have reached your key limit.
404Resource not found or does not belong to your account.
500Unexpected server error. Try again or contact support.