# Botdoor > Botdoor (https://botdoor.co) gives AI bots secure access to your social media. A bot creates posts through a REST API, an MCP server, or plain-HTML /agent pages. A human connects each social account once and stays in full control: approve every post, or let trusted bots post on their own. New keys ask first; only a signed-in person can change that. Networks: Instagram, Facebook, TikTok, YouTube, X (platform id "twitter"; Solo plan and up), LinkedIn, Threads, Pinterest, Reddit and Bluesky. Post types: video, photo, carousel (several mediaIds), text, thread and poll. Instagram, TikTok, YouTube and Pinterest need an image or video. Threads are only X, Threads and Bluesky. Polls are only X and LinkedIn. Full documentation: https://docs.botdoor.co (every endpoint, MCP tool and error code; as one text file: https://docs.botdoor.co/llms-full.txt). Auth for everything below: an API key (create one in the app under API keys), sent as "Authorization: Bearer bd_...". Keys act on one workspace only. Keys made before October 2026 start with msx_ and keep working. ## Start here (do this first) 0. No Botdoor account and no key? POST https://botdoor.co/api/v1/signup with {"email": "", "workspaceName"?, "agentName"?} (no auth; MCP: the signup tool, offered when you connect without a key). It creates a Free workspace and returns 201 with apiKey (shown once, asks first), connectUrl and next. Botdoor emails the claim link to that address (claimSent: true); you never see it. Ask your human to check their inbox. The link is single-use and expires in 7 days. They claim (set a password), then connect an account from https://botdoor.co/start. Connecting is refused with 403 claim_required until they claim, and you can upload at most 10 files before then. Until they claim it nothing can publish (posts wait for their approval), and an unclaimed workspace is deleted after 7 days. No email? POST /api/v1/claim-link with your key (MCP: new_claim_link) to email a fresh one (2 a day, and not within 10 minutes of the last one); the old one stops working and the 7 days are not extended. When your human claims, they choose whether your key keeps access. A 202 with apiKey null means no key was issued: we emailed that address instead (if it already has a workspace, your human signs in at https://botdoor.co/login and creates a key for you under API keys). Limits: 5 signups an hour per IPv4 address or IPv6 /48, 20 an hour per IPv4 /24 or IPv6 /32, 10 a day per IPv4 /24 or IPv6 /48, 3 a day per email (429 rate_limited), a daily cap for everyone (503 signup_capacity), no disposable addresses (400 email_not_allowed). Names with control, zero-width or text-direction characters are refused (400 validation_failed). 1. GET /api/v1/accounts (MCP: list_accounts). If "accounts" is empty, nothing can be posted yet. The reply has "next" telling you what to do. 2. No account: pick a platform and call GET /api/v1/connect/{platform} (MCP: get_connect_url). It returns {url}. Send that url to your human and ask them to sign in. You cannot sign in for them. Or send them to https://botdoor.co/start, where they can connect in one click. 3. Call GET /api/v1/accounts again until the account shows up. Use its "id" as accountIds. 4. Create the post. With an asks-first key (the default) it waits as needs_approval until your human approves it in Botdoor. Tell them it is waiting. 5. Posting with no account connected returns 409 no_accounts_connected. details.connectUrl is the page for your human; details.connectApi is the endpoint that returns a sign-in url. ## MCP (recommended for tool-calling agents) - https://botdoor.co/api/mcp : streamable HTTP, stateless, JSON replies. - Tools: list_accounts, get_connect_url, new_claim_link (unclaimed signups only), upload_media, create_post, get_post, list_posts, approve_post, reject_post, cancel_post, reschedule_post, retry_first_comment. - Discovery: server card https://botdoor.co/api/mcp/server-card (also /.well-known/mcp-server-card), AI Catalog https://botdoor.co/.well-known/ai-catalog.json, MCP Registry name co.botdoor/botdoor. Every tool declares readOnlyHint, destructiveHint, idempotentHint and openWorldHint. - Typical flow: list_accounts, upload_media (optional), create_post with dryRun=true, create_post, get_post until settled=true. - scheduledFor is an ISO 8601 timestamp with a timezone offset, for example 2026-10-12T15:00:00-04:00. It is passed to Zernio as scheduledFor. Do not send a time with no offset. The date must be real (no 31 Feb, no T24:00) and the offset between -12:00 and +14:00, else invalid_date (the message says what is wrong). A string that is not ISO 8601 with an offset is validation_failed. A past time is rejected (scheduled_in_past), and more than a year ahead is rejected (scheduled_too_far). Nothing is published in either case. - A post that needs approval is held locally. Approving before scheduledFor schedules it at Zernio. Approving after that time, or less than a minute before it, returns 409 schedule_passed and does not post. Choose publishNow true (Publish now) or a new scheduledFor (Pick a new time). - Links: put them in firstComment, which keeps them out of the main post on every platform that supports it. thread (X, Threads and Bluesky only) publishes follow-up posts with the main post; thread[0] is the first reply after content. firstReply is an alias for a one-item thread. Do not send both. The stored field is thread. - An asks-first key that reschedules an already scheduled (approved) post sends it back to needs_approval: it is removed at Zernio and waits for a person again. A key that posts on its own just moves the time. - An asks-first key always waits for approval. requireApproval=false does not publish immediately for that key. requireApproval=true always waits. - Approving needs a signed-in person. Any API key gets 403 approval_requires_human. The key that created the post gets self_approval_forbidden. A key may still reject, including its own post. - dryRun validates only. It does not count toward the Free 30-post monthly cap, and it is refused once that cap is already used. ## REST (https://botdoor.co/api/v1) - GET /accounts : connected accounts. Each has id, platform, username and avatarUrl. Several accounts may share a platform. Use "id" as accountIds so the post goes to that @handle. - GET /connect/{platform}?redirect_url=... : platform ids are instagram, facebook, tiktok, youtube, twitter (X), linkedin, threads, pinterest, reddit, bluesky. returns {url}; a human opens it to connect an account. redirect_url must be a path on this site, or an absolute URL with the same origin as APP_URL. Another account on a platform that is already connected is allowed until the plan's account total. Reconnect one account with ?reconnect={zernioAccountId}; the id must be one of this workspace's accounts on that platform (else 404 account_not_found). - POST /media : multipart "file", or JSON {filename, contentType, dataBase64}. Returns media.id. Accepted only when the bytes are a JPEG, PNG, GIF, WebP, MP4, MOV, WebM, M4V, MPEG or AVI and the width and height can be read. A filename or contentType that does not match the bytes is rejected; the error names the declared type or the filename. A truncated file is rejected too. - POST /posts : JSON {content, mediaIds[], accountIds[], dryRun, requireApproval, thread[], firstReply, scheduledFor, poll, cover:{mediaId|offsetMs}, tiktokAiGenerated, firstComment}. Send an Idempotency-Key header. The same key and body returns the original post. A different body with that key is 422 idempotency_key_reused. scheduledFor must include a timezone offset. Put links in firstComment. - firstComment (string): put links in firstComment. The main post is published first. About 10 seconds after Zernio confirms a platform post id (FIRST_COMMENT_DELAY_MS, default 10000, plus FIRST_COMMENT_JITTER_MS, default 2000), the same account replies to that id. The wait is a row in Postgres (next_attempt_at), not an in-memory timer, so a restart does not lose it or post it twice. GET /posts/{id} reports firstComment.status as pending, posted, failed, or skipped, and each target's url. Poll until settled is true. If the post fails, the comment is not sent (skipped). If the comment fails after retries, the post stays published and firstComment.status is failed; POST /posts/{id}/first-comment/retry (or MCP retry_first_comment) tries again. One row per target and a stable Idempotency-Key mean a comment is never posted twice. - firstComment length limits, checked at create (400 validation_failed, details.limits): X 280, TikTok 150, Threads 500, LinkedIn 1250, Instagram 2200, Facebook 8000, YouTube 10000 characters. - First comments are sent by the server in the background (checked every 15 seconds); no GET is needed. - firstComment platforms: X, LinkedIn, Instagram, Facebook, Threads, YouTube, TikTok. Any other target is 400 first_comment_unsupported with details.platforms. It can be set together with thread: thread still publishes with the post, and firstComment is a later reply to the root, not an extra thread item. - Zernio's own platformSpecificData.firstComment (Instagram, Facebook, LinkedIn, Threads, YouTube) posts during the create call and can skip the comment when the account cannot comment. This API does not use that field. It waits for platformPostId, then calls Zernio POST /v1/inbox/comments/{platformPostId} with the commentId omitted (a reply to the post). X has no firstComment field; threadItems would publish the reply in the same request, so X uses that same comments call. - cover must be an object. cover.mediaId is an uploaded image. It is sent as Instagram instagramThumbnail, TikTok video_cover_image_url, Facebook and YouTube mediaItems.thumbnail, and Pinterest coverImageUrl. cover.offsetMs (milliseconds) is sent as Instagram thumbOffset, TikTok video_cover_timestamp_ms, and Pinterest coverImageKeyFrameTime (whole seconds). The image wins when both are set. offsetMs past the video's duration is rejected. - tiktokAiGenerated must be a boolean. true sets TikTok video_made_with_ai and requires a TikTok account. - GET /posts, GET /posts/{id} : status per platform, live URLs, scheduledFor (UTC instant, or null), "settled" says when to stop polling. A future scheduled post is settled until its time passes. - POST /posts/{id}/approve : for status needs_approval. Body may be {}. publishNow true publishes now. scheduledFor sets a new time. If the stored time has passed and neither is sent, the response is 409 schedule_passed and the post stays needs_approval. Approve requires a signed-in person (a key gets approval_requires_human; the author key gets self_approval_forbidden). - POST /posts/{id}/reject : for status needs_approval. Reject is open to keys. - POST /posts/{id}/cancel : cancel before it goes out (needs_approval or scheduled). A scheduled post is deleted at Zernio. - POST /posts/{id}/reschedule : JSON {scheduledFor} with an offset. Waiting posts stay local. Scheduled posts are updated at Zernio (PUT scheduledFor). - Bulk approve and reject exist only on the signed-in Posts page (Approve selected / Reject selected). There is no bulk endpoint for API keys. - Errors: {"error": {"code", "message", "hint", "retryable", "details"}}. Switch on "code". A 401 also sends WWW-Authenticate: Bearer. ## Computer-use / browser agents: https://botdoor.co/agent Plain HTML that works with JavaScript off (one small script only stops double submits), one action per page, stable URLs. Add ?format=json to any GET page for the same data as JSON. Auth: the signed-in browser session, or an API key (Authorization: Bearer ..., or HTTP Basic with the key as the password). - /agent : plan usage and links - /agent/posts : all posts with full ids, STATUS and live URLs - /agent/calendar : needs approval, scheduled, and published posts grouped by day. ?span=week, ?at=YYYY-MM-DD, ?tz=IANA. ?format=json returns the same list. - /agent/posts/new : create a post (form; supports file upload and dry run) - /agent/posts/{id} : one post - /agent/posts/{id}/approve and /agent/posts/{id}/reject : decide a post that needs approval. If the scheduled time has passed, approve offers Publish now and Pick a new time. - /agent/posts/{id}/cancel and /agent/posts/{id}/reschedule : stop or move a post before it goes out. SCHEDULED FOR on /agent is the UTC instant. - /agent/accounts : accounts and plan usage - /agent/accounts/connect/{platform} : redirects to the platform login (you finish it), then returns to /agent/accounts - /agent/keys : API keys (changing keys needs a signed-in session) Plans: Free (1 account, no X, 30 posts a month), Solo (3 accounts, +extra), Agency (15 accounts, +extra).