Getting Started
Posthive Documentation
Schedule posts to multiple platforms from one interface with built-in MCP support for AI agents.
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.
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.
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
- Write your post in the Compose page.
- Click + Save in the POST section header.
- Enter a name and press Enter or click Save. Template names must be unique.
Loading a template
- Click Templates in the POST section header to open the dropdown.
- 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.jpgPreview and scheduling
- Paste your CSV or upload a .csv file.
- Click Preview — each row is validated and shown in a table with its status (✓ Ready or ✕ error).
- Review the results. Error rows are skipped automatically.
- 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 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 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 --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) or in Supabase Storage (prod). 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.
Supabase 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 all saved post templates |
| Create Post from Template | Action | Create 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:
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 |
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.
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/mcp3. 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.
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/mcp3. 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.
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 |
| 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 saved post templates | — |
| create_from_template | Draft 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:
| 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.”
“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.”
“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. |