# dmswift > Run your Facebook Page and Instagram from your AI agent - plus a customer inbox on Meta's official APIs that only replies to people who wrote to you first. dmswift is a web application and an MCP server for businesses on Facebook and Instagram. An owner talks to their own AI agent (Claude, ChatGPT, Gemini, Grok, Perplexity, Cursor, n8n, or an agent they message on WhatsApp) and the agent publishes and schedules posts, starts comment-to-DM campaigns, moderates comments, reads insights and answers the inbox through dmswift. The same work can be done by hand in the web dashboard. Key facts: - Channels: Facebook Pages and Instagram professional accounts (business or creator), through Meta's official APIs. Instagram connects directly with Instagram Login (no Facebook Page needed), or through a linked Facebook Page with Facebook Login. Facebook Groups and personal Instagram accounts are not supported. WhatsApp, TikTok and other networks are not managed channels. - "Talk to your agent on WhatsApp": the owner messages an AI agent on WhatsApp (for example Poke, Instinct, OpenClaw, NanoClaw, or Meta's Muse through the dmswift API) and that agent runs the Page and Instagram through dmswift. WhatsApp is only where the owner talks to the agent; dmswift does not read or send WhatsApp messages itself. Meta AI (the assistant built into WhatsApp, Instagram and Facebook) cannot add custom tools, so it cannot connect. - Private messages only go to people who wrote first (a comment or a message), inside Meta's 24-hour window. Follow-ups to recent conversations go only to people who opted in or wrote first, stay inside that window, and are not available to agents. - MCP server URL: https://app.dmswift.com/mcp (remote, Streamable HTTP; sign-in is OAuth 2.1, or a per-account API key). 44 tools. MCP and API access is included from Starter ($19/month) up. Product: https://app.dmswift.com. REST API and OpenAPI spec: https://app.dmswift.com/openapi.json. - Agent safety: per-connection daily caps (default 15 publishes, 30 comments, 100 messages), scopes per key, and an optional "ask me before anything is published" approval mode. - Pricing is in US dollars and metered by what is sent, never by contacts. Agent actions are write calls made by API/MCP keys; reads are free. 7-day Pro trial with no card, once per business; there is no free plan, and when the trial ends the account is paused (automations stop; sign-in, viewing and export keep working) until a plan is chosen. 14-day money-back on any charge. Paddle is the Merchant of Record and bills monthly or annually (annual = two months free). - What the limits count: a private message is one dmswift sends automatically (a campaign reply, a broadcast or sequence message, a growth-link greeting, an Instagram mention reply), never a reply a person types in the inbox and never the message sent when someone taps a button; campaigns count every saved campaign, active or paused, across all channels; posts count what dmswift publishes on Facebook or Instagram in the month (drafts, uploads and scheduling are unlimited); follow-up broadcasts count each broadcast started or scheduled in the month (a draft costs nothing); AI replies are not included on Starter; team seats are the people invited besides the owner. - Interface in English and Hebrew (right-to-left). - Operated by Noam Shmuel Nissan, sole proprietor, trading as dmswift (Israel). Contact: support@dmswift.com. ## Product - [Features](https://dmswift.com/features): Comment-to-DM, DM automations, a unified inbox, contacts, a Facebook and Instagram post scheduler, analytics, moderation and team seats, on Meta's official APIs. - [Comment to DM](https://dmswift.com/features/comment-to-dm): Answer a matching comment on a post or reel with a public reply and one private message, on Instagram and Facebook, through Meta's Private Replies API. - [DM keyword automations](https://dmswift.com/features/dm-automation): Answer Instagram and Messenger messages by keyword inside Meta's 24-hour window, with buttons, tracked links and optional AI replies from your knowledge base. - [Instagram automation](https://dmswift.com/features/instagram-automation): Comment-to-DM, story replies, mentions, DM keywords and publishing for Instagram professional accounts, on Meta's official API. No password, no browser bot. - [Facebook automation](https://dmswift.com/features/facebook-automation): Answer Facebook Page comments with a public reply and a Messenger message, automate Messenger by keyword, schedule posts and keep the comment section clean. - [Unified inbox](https://dmswift.com/features/unified-inbox): One inbox for Instagram DMs and Messenger, on Meta's official APIs, that only replies to people who wrote first. Human takeover, team roles, agent replies. - [Contacts CRM](https://dmswift.com/features/contacts-crm): Every person who commented on or messaged your Page or Instagram account, with tags, saved segments, custom fields, notes and CSV export. Unlimited on every plan. - [Post scheduler](https://dmswift.com/features/post-scheduler): Draft, schedule and publish Facebook posts and Instagram feed posts, carousels, reels and stories, with a first comment, from the dashboard or your AI agent. - [Analytics and insights](https://dmswift.com/features/analytics): A funnel from comment to private message to click, per campaign, post and day, plus Meta insights for every channel and post, and the questions left unanswered. - [Comment moderation](https://dmswift.com/features/comment-moderation): Hide link spam, follow-back bait, all-caps floods, profanity and your own blocklist on Facebook and Instagram, automatically, with an allowlist and an undo log. - [Team and agency](https://dmswift.com/features/team-agency): Invite your team with viewer, agent and admin roles, run up to 25 client workspaces on the Agency plan, and put your own name, logo and domain on the dashboard. - [Pricing](https://dmswift.com/pricing): Starter $19, Pro $49, Business $129 and Agency $299 a month, with a 7-day Pro trial. Metered by messages, posts and agent actions, never by contacts. ## AI agents and MCP - [MCP server](https://dmswift.com/mcp): A hosted MCP server for Facebook Pages and Instagram: 44 tools to publish, schedule, run comment campaigns, moderate, read insights and answer the inbox. - [Claude](https://dmswift.com/mcp/claude): Add dmswift as a custom connector in Claude or Claude Code, sign in, and let Claude publish, run comment campaigns and report on your Page and Instagram. - [ChatGPT](https://dmswift.com/mcp/chatgpt): Add dmswift to ChatGPT as a custom MCP plugin with OAuth, then have ChatGPT or your dot schedule posts, run comment campaigns and report on your channels. - [Claude Duet](https://dmswift.com/mcp/claude-duet): Run your Facebook Page and Instagram from Claude Duet. Connect dmswift in Claude once, and Duet publishes, answers comments and runs comment-to-DM campaigns. - [Cursor](https://dmswift.com/mcp/cursor): Add dmswift to Cursor's mcp.json with OAuth or an API key, then draft posts, read comments and build campaigns for your Facebook Page and Instagram from the editor. - [n8n](https://dmswift.com/mcp/n8n): Connect n8n's AI Agent to dmswift with the MCP Client Tool and an API key, and build workflows that publish, run comment campaigns and report. - [WhatsApp agent](https://dmswift.com/mcp/whatsapp-agent): Message an AI agent on WhatsApp, iMessage or Telegram and it runs your Facebook Page and Instagram through dmswift: Poke, Instinct, OpenClaw, NanoClaw or Muse. - [Poke](https://dmswift.com/mcp/poke): Add dmswift to Poke, the AI agent you text on iMessage, SMS, Telegram or WhatsApp, and run your Facebook Page and Instagram from your messages. - [OpenClaw](https://dmswift.com/mcp/openclaw): Add dmswift to OpenClaw, the open-source self-hosted agent, over Streamable HTTP with OAuth or an API key, and run your Page and Instagram from WhatsApp. - [Gemini](https://dmswift.com/mcp/gemini): Add dmswift to the Gemini app as a custom app over MCP, sign in once, and ask Gemini to schedule posts, run comment campaigns and report on your Page. - [Grok](https://dmswift.com/mcp/grok): Connect Grok to dmswift's MCP server, as a custom connector on grok.com or by asking Grok Bot in chat, and run your Facebook Page and Instagram. - [Perplexity](https://dmswift.com/mcp/perplexity): Add dmswift to Perplexity as a custom remote connector with OAuth, then ask Perplexity to schedule posts, run comment campaigns and report on your channels. - [Instinct](https://dmswift.com/mcp/instinct): Ask Instinct to connect to dmswift, send it the MCP link, and run your Facebook Page and Instagram from your messages. Tested by dmswift. - MCP tools: get_account_status, list_pages, check_page_permissions, list_live_posts, list_posts, list_scheduled_posts, get_post, upload_media, request_upload_link, get_uploaded_media, render_text_card, get_brand_kit, set_brand_kit, create_post, update_post, duplicate_post, delete_post, publish_post, comment_on_post, list_comments, reply_to_comment, hide_comment, delete_comment, like_comment, list_campaigns, get_campaign, create_campaign, update_campaign, set_campaign_active, delete_campaign, test_campaign_match, list_conversations, get_conversation, reply_to_conversation, get_stats, get_activity_log, get_analytics, get_unanswered_questions, get_insights, get_post_insights, list_contacts, tag_contact, search_knowledge, add_knowledge ## Help guides - [Help guides](https://dmswift.com/guides): Step-by-step dmswift help guides: connect a Facebook Page, connect an Instagram professional account, and fix the snags people hit most often. - [Connect a Facebook Page](https://dmswift.com/guides/connect-facebook-page): Connect a Facebook Page to dmswift with Facebook Login: the steps, what each permission is for in plain words, Business portfolios, reconnecting and fixes. - [Connect Instagram](https://dmswift.com/guides/connect-instagram): Connect an Instagram professional account to dmswift directly with Instagram Login, no Facebook Page needed, or through a linked Page. Steps and fixes. ## Docs - [API and MCP reference](https://dmswift.com/docs): authentication, scopes, every MCP tool and REST route, webhooks, errors. - [Full text for LLMs](https://dmswift.com/llms-full.txt): this file plus the whole site and the API reference in one document. - [OpenAPI spec](https://app.dmswift.com/openapi.json) ## Plans (USD) - Starter: $19/month or $190/year - Connect your AI agent (MCP): full MCP and API access, 300 agent write-actions a month, reads free, 1 key; 2 channels; 3,000 automated private messages a month; Up to 10 campaigns at a time, active or paused; 50 posts a month published through dmswift (Facebook + Instagram); 2 follow-up broadcasts a month; AI replies not included (Pro and up); You plus 1 teammate; Unlimited contacts - Pro: $49/month or $490/year - 5 channels; 15,000 automated private messages a month; Unlimited campaigns; 200 posts a month published through dmswift (Facebook + Instagram); 30 follow-up broadcasts a month; 2,000 AI replies a month; MCP and API: 2,000 agent actions a month, 3 keys; You plus 3 teammates, with viewer, agent and admin roles; Unlimited contacts - Business: $129/month or $1,290/year - 10 channels; 100,000 automated private messages a month; Unlimited campaigns; 600 posts a month published through dmswift (Facebook + Instagram); 100 follow-up broadcasts a month; 2,000 AI replies a month; MCP and API: 10,000 agent actions a month, 10 keys; You plus 10 teammates; Priority support - Agency: $299/month or $2,990/year - 30 channels; 25 client workspaces; White-label: your name, logo, colour and domain; 300,000 automated private messages a month; Unlimited posts (fair use); Unlimited follow-up broadcasts and AI replies; MCP and API: 40,000 agent actions a month, unlimited keys; Unlimited teammates - Founding price: Pro at $39/mo, price locked while you stay subscribed - first 100 customers or until Dec 31, 2026. ## Company - [About](https://dmswift.com/about): dmswift is built and run in Israel by Noam Shmuel Nissan. What the product is for, the principles behind it, and how to reach the person who builds it. - [Security](https://dmswift.com/security): How dmswift protects your Meta tokens and data, limits what AI agents and API keys can do, signs webhooks, backs up, deletes data, and how to report a vulnerability. - [Support](https://dmswift.com/support): How to reach dmswift support, how fast we answer on each plan, and where to find the docs, the MCP guides, billing help and the privacy and data deletion pages. - [Changelog](https://dmswift.com/changelog): Every notable change to dmswift, newest first: the MCP server, Instagram publishing, agent safety controls, insights, plans and pricing, and the new marketing site. - [Glossary](https://dmswift.com/glossary): Plain definitions of the terms used by dmswift and Meta's messaging rules: MCP, agent actions, comment-to-DM, private replies, the 24-hour window and scopes. - [privacy](https://dmswift.com/privacy) - [terms](https://dmswift.com/terms) - [acceptable-use](https://dmswift.com/acceptable-use) - [refund](https://dmswift.com/refund) - [subprocessors](https://dmswift.com/subprocessors) - [data-deletion](https://dmswift.com/data-deletion) - [accessibility](https://dmswift.com/accessibility) ## Optional - [dmswift vs ManyChat](https://dmswift.com/compare/manychat): ManyChat is a multi-channel chat marketing platform priced by contacts. dmswift is a Facebook and Instagram tool with its own MCP server, priced by what you send. - [dmswift vs Inrō](https://dmswift.com/compare/inro): Inrō is an Instagram-only DM tool with an MCP server, priced in euros by active contacts. dmswift adds Facebook and publishing, and prices by sends. - [dmswift vs LinkDM](https://dmswift.com/compare/linkdm): LinkDM automates Instagram and Facebook DMs, priced by DMs. dmswift also prices by sends, and adds publishing, moderation, insights and an MCP server. - [dmswift vs CreatorFlow](https://dmswift.com/compare/creatorflow): CreatorFlow is a flat-rate, Instagram-only DM automation tool. dmswift covers Facebook and Instagram, publishing, moderation and an MCP server for AI agents. - [dmswift vs Ayrshare](https://dmswift.com/compare/ayrshare): Ayrshare is a developer API for posting to 14+ social networks, with a hosted MCP server, from $149 a month. dmswift focuses on Facebook and Instagram, from $19. --- # The site, page by page Source: https://dmswift.com/about # A small product with a narrow job, done properly. dmswift lets a business run its Facebook Page and Instagram account from the AI agent it already talks to, and answers the people who write to it, on Meta's official APIs. ## Why it exists Most small businesses get their customers' questions in comments and DMs, and most of those questions are the same ten. Answering them by hand all day is what burns owners out; answering them with a tool that messages people who never asked is what gets accounts restricted. dmswift does the first without the second: it answers people who wrote first, inside Meta's rules, and it lets the owner direct all of it by talking to an agent instead of learning another dashboard. ## What we hold ourselves to - Official APIs only. No browser automation, no stored passwords, nothing that pretends to be you. - Only people who wrote first get a private message. Follow-ups go to people who opted in or wrote first. - Prices that do not grow with your audience: we meter what is sent, never contacts. - Limits stated next to features, not hidden in a help center. - Hebrew and right-to-left done properly, not as an afterthought. ## Who runs it dmswift is operated by Noam Shmuel Nissan, a sole proprietor in Israel, trading as dmswift. Postal address: 4 HaKomemiut St., Shomron, Israel. Dodo Payments is our Merchant of Record for payments, tax and invoices. - Support: support@dmswift.com - Privacy: privacy@dmswift.com - Accessibility: accessibility@dmswift.com ## Independence dmswift is an independent product. It is not affiliated with, endorsed by or sponsored by Meta Platforms, Inc. Facebook and Instagram are trademarks of Meta Platforms, Inc. --- Source: https://dmswift.com/changelog # What is new in dmswift Notable changes, newest first. The API reference always describes the current behavior. --- Source: https://dmswift.com/compare/ayrshare # dmswift vs Ayrshare Ayrshare is infrastructure for developers who post to many networks. dmswift is a finished product for Facebook and Instagram that also happens to be an MCP server. ## At a glance - Built for | Businesses and agencies on Facebook and Instagram | Developers building social features into their own apps - Networks | Facebook Pages and Instagram | 14+ social networks - Entry price | Starter $19/month, after a 7-day Pro trial | Premium $149/month, 1 profile - Many profiles | Agency $299/month: 30 channels, 25 client workspaces | Launch $299/month up to 10 profiles; Business from $599 for 30 - MCP server | Yes, OAuth sign-in for Claude and ChatGPT | Yes, hosted, API-key based - Comment-to-DM campaigns | Yes | No; comments and messages are available through the API - Dashboard for non-developers | Yes, in English and Hebrew | Developer-oriented ## Where Ayrshare is better - Far more networks from one API: if you post to X, LinkedIn, TikTok and others, it covers them. - Built for developers who need to give their own users social posting at scale, with profiles and sub-profiles. - Unlimited posts on every plan, as listed. ## Where dmswift is different - A product, not just an API: comment-to-DM campaigns, a unified inbox, contacts, moderation and analytics come with it. - Sign-in instead of keys for Claude and ChatGPT: paste the URL, approve on a consent screen. - A much lower entry price for one business with a Page and an Instagram account. ## Which to choose Choose Ayrshare to add multi-network posting to your own software. Choose dmswift to run your own, or your clients', Facebook and Instagram from a dashboard or an agent. --- Source: https://dmswift.com/compare/creatorflow # dmswift vs CreatorFlow CreatorFlow is a simple, inexpensive comment-to-DM tool for Instagram creators. dmswift costs a little more and covers Facebook, publishing and your AI agent. ## At a glance - Channels | Facebook Pages and Instagram | Instagram only - Priced by | Messages, posts and agent actions; contacts unlimited | Flat rate per plan, no per-contact fees - Free option | No free plan; a 7-day Pro trial | 1 Instagram account, 500 DMs a month - Entry paid price | Starter $19/month: 2 channels, 3,000 DMs | Pro $15/month ($12 billed annually): 2 accounts, 5,000 DMs - Next tier | Pro $49: 5 channels, 15,000 DMs, you plus 3 teammates | Growth $30/month: 5 accounts, 10,000 DMs, 5 seats - Trial | 7 days of Pro, no card | 14 days, payment method required - MCP server | Yes, first-party | Not listed on the pricing page ## Where CreatorFlow is better - A lower price and more DMs on its entry plan. - A longer trial (14 days), though it asks for a payment method. - Creator-focused extras such as follow gating and email collection. ## Where dmswift is different - Facebook Pages too, with Messenger in the same inbox. - Publishing and scheduling for Facebook and every Instagram format. - An MCP server so your own AI agent can run campaigns, publish, moderate and report. - No card for the trial, and a 14-day money-back guarantee on your first payment. ## Which to choose Choose CreatorFlow for a low-cost, Instagram-only comment-to-DM setup. Choose dmswift if you also run a Facebook Page, publish from the same place, or want an agent to do the work. --- Source: https://dmswift.com/compare/inro # dmswift vs Inrō Inrō is a focused Instagram tool and, like dmswift, has an MCP server for Claude and ChatGPT. The differences are platforms, publishing and what the price is metered on. ## At a glance - Channels | Facebook Pages and Instagram | Instagram only - Priced by | Messages, posts and agent actions; contacts unlimited | Active contacts per month - Free option | No free plan; a 7-day Pro trial, no card | 100 active contacts a month, 3 scenarios, 3 campaigns a month - Entry paid price | Starter $19/month | Pro €12.99/month (500+ active contacts, scaling) - MCP server | Yes, 44 tools incl. publishing and moderation | Yes, 25+ tools for contacts, campaigns and conversations, per Inrō - Post publishing | Facebook and Instagram, every format | Not listed - Currency | USD | EUR ## Where Inrō is better - A cheaper entry price for a single Instagram account with a small audience. - AI intent detection on comments and a very short setup, focused on Instagram creators. - An MCP server too, so Claude and ChatGPT can already work with your Inrō data. ## Where dmswift is different - Facebook as well as Instagram, in one inbox and one contact list. - Publishing and scheduling for Facebook and every Instagram format, so an agent can post and then run the campaign on that post. - Comment moderation and Meta insights through the same MCP server. - Contacts never metered, so a larger audience does not change the bill. ## Which to choose Choose Inrō for a simple, Instagram-only setup on a small budget. Choose dmswift if you also run a Facebook Page, want publishing in the same tool, or want an agent to handle posts, comments and the inbox end to end. --- Source: https://dmswift.com/compare/linkdm # dmswift vs LinkDM LinkDM and dmswift both meter messages rather than contacts and both cover Instagram and Facebook. LinkDM is more generous on volume; dmswift does more around it. ## At a glance - Channels | Facebook Pages and Instagram | Instagram and Facebook - Priced by | Messages, posts and agent actions | DMs per month - Free option | No free plan; a 7-day Pro trial, no card | 1 Instagram account, 1,000 DMs a month - Around $19/month | Starter $19: 2 channels, 3,000 DMs, MCP and API | Pro $19: 3 accounts, 25,000 DMs - Higher tier | Pro $49: 5 channels, 15,000 DMs | Platinum+ $99: 10 accounts, 300,000 DMs - MCP server | Yes, first-party | Not listed on the pricing page; API on Enterprise - Post publishing | Facebook and Instagram, every format | Not listed ## Where LinkDM is better - More private messages per dollar on every paid tier. - A free plan for a single Instagram account; dmswift has a 7-day trial instead. - Annual discounts of 20 to 30 percent, as listed. ## Where dmswift is different - An MCP server on every paid plan, so Claude, ChatGPT or a WhatsApp agent can run campaigns, publish and report. - Publishing and scheduling for Facebook and every Instagram format, with campaigns attached to posts. - Moderation, insights and a unified inbox with human takeover and team roles. - Hebrew and right-to-left throughout. ## Which to choose Choose LinkDM if raw DM volume at the lowest price is what matters. Choose dmswift if you want one tool for posting, answering and moderating, and an agent that can do all three. --- Source: https://dmswift.com/compare/manychat # dmswift vs ManyChat ManyChat is the best-known tool in this category and covers far more channels. dmswift does less, on two platforms, and is built to be run by your own AI agent. ## At a glance - Channels | Facebook Pages and Instagram | Instagram, Messenger, WhatsApp, TikTok, Telegram, SMS and email - Priced by | Messages, posts and agent actions; contacts unlimited | Active contacts in the month, with per-contact overage - Entry paid price | Starter $19/month (2 channels, 3,000 DMs) | Pro from about $29/month billed annually for 2,500 active contacts, as reported - Built-in AI | AI replies from your knowledge base | Manychat AI on Pro and higher, as reported - MCP server for agents | Yes, first-party, OAuth sign-in, 44 tools | No first-party server found; third-party wrappers such as Zapier MCP - Post publishing | Facebook and Instagram, incl. reels, stories, carousels | Not part of the plans we checked ## Where ManyChat is better - Many more channels, including WhatsApp, TikTok, SMS and email, in one tool. - A mature visual flow builder for multi-step conversations, with a large template library. - A big ecosystem of agencies, courses and integrations, and years of track record. ## Where dmswift is different - The bill does not grow with your audience. Contacts are never metered; what you send is. - Your agent runs it. A first-party MCP server lets Claude, ChatGPT or a WhatsApp agent publish, run campaigns and report, with daily caps and an approval mode. - Publishing is in the same tool, for Facebook and every Instagram format, with comment campaigns attached to the post. - A dashboard in English and Hebrew, with a proper right-to-left layout. ## Which to choose Choose ManyChat if you need WhatsApp, TikTok or SMS automation, or complex visual flows. Choose dmswift if you live on Facebook and Instagram, want your AI agent to do the work, or have a large audience you do not want to pay per head for. --- Source: https://dmswift.com/compare # How dmswift compares, including where others are better. Each page states what the other product does well, what dmswift does differently, and the public sources we checked, with the date we checked them. ## What we compare on - Platforms: Facebook Pages, Instagram professional accounts, and anything else the tool covers. - What the price is metered on: contacts, messages, profiles or posts. - Whether an AI agent can run it over MCP, and with what safety limits. - How it reaches Meta: the official APIs, with permissions you can revoke. Prices change. Every comparison links to the public pages we checked and the date we checked them; if something is out of date, tell us at support@dmswift.com and we will correct it. --- Source: https://dmswift.com/docs # API and MCP reference Written to be handed, as it is, to an agent or a developer. The MCP endpoint is https://app.dmswift.com/mcp; the machine-readable spec is the OpenAPI document on the app host. The same reference as plain Markdown, for an agent: llms-full.txt. The OpenAPI spec: openapi.json. --- Source: https://dmswift.com/features/analytics # What each post brought in, counted the same way every time. One funnel, comment to public reply to private message to click, broken down by campaign, by post and by day, next to the reach and engagement numbers Meta reports for the channel. ## The funnel - Comments received and matched, public replies sent, private messages delivered, and link clicks, per campaign. - Period-over-period change on the headline numbers, and the hours and days your audience responds most. - A/B results per message variant, with no winner declared before there is enough data. - Every number downloads as CSV. Group comments are never counted, because nothing can be sent in reply to them. ## Meta insights For each channel: followers, reach, impressions or views, engagements and profile views where Meta reports them, by day, week or 28 days. For each live post: reach, views, likes, comments, shares and, on Instagram, saves. These come straight from Meta's insights API with the read_insights and instagram_manage_insights permissions. ## Unanswered questions Comments that matched no campaign are deduplicated and ranked by how often they were asked. Each one is a campaign worth creating, and it is the first thing an agent asked to "improve this page" should read. ## Reports that come to you Ask your agent for the week and it calls get_analytics, get_stats, get_unanswered_questions and get_insights, then writes up the three numbers that matter and two next steps. There is a ready-made prompt for it, weekly_performance_report. Q: Are the dashboard numbers the same as Meta's? A: The funnel is dmswift's own count of what it did. Reach, views and engagement are Meta's numbers, shown as Meta reports them. Q: Can I export the data? A: Yes. Every report downloads as CSV, and events can stream to your own system through signed webhooks. Q: Do reads by my agent cost anything? A: No. Reading statistics, insights and logs never counts as an agent action. --- Source: https://dmswift.com/features/comment-moderation # A clean comment section, without reading every comment. The same pass that answers a matching comment hides the ones you do not want under your posts. It hides rather than deletes, logs every decision, and lets you undo any of them. ## What it catches - Link-shortener spam and comments that are nothing but a link. - Follow-back and giveaway bait. - All-caps floods and repeated characters. - Profanity, and every word on your own blocklist. - An allowlist always wins: a word you allowed is never hidden. ## Why hide instead of delete A hidden comment disappears for everyone else, but its author is not notified, and on Facebook it stays visible to them and their friends, so there is nothing for them to argue about. You can unhide it from the log in one click. ## What it is, honestly The classifier is lexical and whole-word, not an AI. That makes it predictable: the same comment always gets the same decision, and the log says which rule made it. ## From your agent With comments:read an agent lists comments on live posts; with comments:moderate it can hide, unhide and delete them. Replying under a comment needs comments:write and counts toward the key's 30 comments a day. Q: Does it delete comments? A: Automatic moderation only hides. Deleting is a separate, deliberate action, from the dashboard or by an agent with comments:moderate. Q: Does it work on old posts? A: It acts on comments as they arrive. Older comments can be reviewed and hidden from the comment list. Q: Can it moderate Facebook Group comments? A: No. Meta does not give Page apps that access to Groups. --- Source: https://dmswift.com/features/comment-to-dm # Someone asks under your post. They get the answer in private. A comment that matches your keywords gets a reply in the thread and one private message to the person who wrote it, usually within a second, on both platforms. ## How a comment campaign works - Pick the trigger: a comment on one post, or on any post on the channel. - Write the keywords to watch for. Require any of them or all of them, and add words that rule a comment out. - Write the public reply that goes under the comment, and the private message that goes to its author, each with its own delay. - Add a button or a link. On Facebook the link can wait behind a follow-up button that only sends it when the person taps. ## Only the person who commented The private message goes to exactly one person: the one who wrote the comment. Meta's Private Replies API allows one private reply per comment, within seven days of it, and dmswift sends at most that. A campaign can also be limited to once per person, to first-level comments, to a daily cap, and to the hours you choose. ## Let your agent build it Ask Claude, ChatGPT or your WhatsApp agent: "see my latest video? create a comment campaign for it". Over MCP it calls list_live_posts, create_campaign with that post's id, and test_campaign_match with a sample comment before it tells you the campaign is live. ## Limits worth knowing - Instagram private messages cap at 1,000 characters and carry no button, so the link is sent inside the text. - Comments in Facebook Groups cannot be answered privately; dmswift works on Pages and Instagram professional accounts. - Some people cannot receive messages at all. Every attempt and every failure is logged with its reason. Q: Does it reply to every comment? A: No. Only to comments that match a campaign's keywords and rules. Everything else is left alone, or moderated if you turned moderation on. Q: How fast is the reply? A: Usually about a second after Meta delivers the comment to us. You can add a delay to the public reply and to the private message separately. Q: Does it work on reels and videos? A: Yes. A comment on a reel or a video is a comment like any other, on Instagram and on Facebook, and a campaign can be bound to that one reel. --- Source: https://dmswift.com/features/contacts-crm # Everyone who ever wrote to you, and you never pay per head. Each person who comments, messages, replies to a story or taps a growth link becomes a contact, on every plan, with no limit and no charge per contact. ## What a contact holds - Name, platform, and how the person first reached you: which campaign, post or growth link. - Tags, added by you, by a campaign, or by your agent with contacts:write. - Custom fields and notes, visible next to the conversation in the inbox. - Whether they are still subscribed to follow-ups, re-checked before every send. ## Segments and follow-ups Save a filter as a segment: everyone tagged "price list" who wrote this month, say. A segment can receive a follow-up now or on a schedule, which only reaches people who opted in or wrote first and who are still inside Meta's window. ## Why contacts are never metered Most tools in this category charge by active contacts, so the bill rises exactly when a post works. dmswift meters what is sent: private messages, posts and agent actions. A post that reaches more people does not change your plan. ## Yours to take Export the list as CSV at any time, or receive every new contact as a signed webhook event in your own system. Deleting the account deletes the contacts with it. Q: Is there a limit on contacts? A: No. Contacts, tags and segments are unlimited on every plan, including the trial. Q: Can my agent read the contact list? A: Yes, with contacts:read. It can search and filter, and with contacts:write it can add tags. Editing or deleting contacts stays with a person. Q: Can I import contacts? A: No. Contacts come from real interactions with your Page or Instagram account, which is also what makes them reachable under Meta's rules. --- Source: https://dmswift.com/features/dm-automation # People who write to you get an answer, even at 3am. A message that contains one of your keywords gets your reply straight away. It only ever answers people who wrote to you first, inside the window Meta allows. ## What starts an automation - An incoming direct message, on Instagram or in Messenger, that contains your keyword. - On Instagram, a reply to one of your stories, or a mention of your account in someone's story. - A tap on a growth link or a printed QR code that opens a conversation with a code you chose. ## What the reply can carry - Text with merge tokens such as the person's first name, and emoji. - Up to three buttons in Messenger, and a follow-up button that sends the link only when tapped. - Tracked short links, so the funnel shows who opened them. - Tags on the contact, so the people who asked for a price list become a segment you can follow up with. ## AI replies from your own words Write your prices, hours and policies into a knowledge base, add your own key for an AI provider (Anthropic, OpenAI, Google Gemini, Groq or OpenRouter), and an AI reply can answer questions no keyword covers. It answers only from those passages, scores its own confidence, and below the threshold you set it sends nothing and hands the conversation to a person. ## The 24-hour window Meta allows a free-form message to a person within 24 hours of their last message to you. dmswift shows how much of that window is left on every conversation, and skips anyone outside it. It never attaches a message tag to get around the rule. Q: Can I message people who never wrote to me? A: No. Every private message dmswift sends answers someone who wrote first: a comment, a message, a story reply or a mention. Q: What happens when a person takes over? A: Automation goes quiet for that contact until the conversation is handed back, so a customer never gets a bot reply in the middle of a human one. Q: Can my AI agent reply in the inbox? A: Yes, with the inbox:write scope, inside the 24-hour window, never while a human has taken the conversation over, and up to 100 messages a day per key. --- Source: https://dmswift.com/features/facebook-automation # A Facebook Page that answers its comments and its Messenger. Connect a Page you manage and dmswift answers the comments and messages that match your campaigns, publishes on your schedule, and hides the spam, all through Meta's Graph API. ## Comments and Messenger - A matching comment gets a public reply and one private message in Messenger, through the Private Replies API. - Messenger messages are answered by keyword, with up to three buttons and a follow-up button that sends the link when tapped. - A person can take any conversation over from the unified inbox; automation pauses for that contact. ## Publishing Text, link, photo and video posts, colored background posts, a first comment published right after the post, and comments as the Page with an image. Posts can be drafted, scheduled through Meta, published now, duplicated and deleted, from the dashboard or by your agent. ## Comment moderation on the same pass The pass that answers a keyword also hides link-shortener spam, follow-back bait, all-caps floods and words from your own blocklist, with an allowlist that always wins. It hides rather than deletes, because the author of a hidden comment is never told. ## Permissions it asks for pages_show_list, pages_read_engagement, pages_read_user_content, pages_manage_metadata, pages_manage_posts, pages_manage_engagement, pages_messaging, read_insights and business_management, granted in Facebook's own dialog and revocable from your Facebook settings at any time. Q: Does it work in Facebook Groups? A: No. Meta does not allow private replies to comments in Groups, so dmswift works on Pages and Instagram professional accounts only. Q: Can it like comments? A: Yes, on Facebook. A campaign can like the comment it answers, and an agent with comments:write can like a comment. Q: Do I need my own Meta app? A: No. You connect through dmswift's own Meta app: press Connect, approve the permissions in Facebook's dialog, and pick the Page. --- Source: https://dmswift.com/features/instagram-automation # Your Instagram account, answered and published from one place. dmswift works with Instagram professional accounts through Meta's official API: replies to comments, story replies and mentions, an inbox for DMs, and publishing for every format. ## What it automates - Comments on posts and reels: A keyword in a comment gets a public reply and a private message to its author. - Story replies: Someone replies to your story with a keyword and gets your answer in the same thread. - Mentions: When an account mentions you in its story, it can be thanked or sent the link it is after. - Incoming DMs: Keyword answers, AI replies from your knowledge base, and a person taking over when it matters. ## Publishing, in every format Feed posts, carousels of 2 to 20 items, reels with a cover and a choice of whether they also appear in the feed, and stories. Each can be scheduled, published now, duplicated or deleted, and can carry a first comment. Instagram publishing comes out of the same post allowance as Facebook on every plan. ## How it connects Two ways, both on Meta's official API. Connect Instagram directly uses the Instagram API with Instagram Login: you sign in to Instagram in Instagram's own window and approve instagram_business_basic, instagram_business_manage_comments, instagram_business_manage_messages, instagram_business_content_publish and instagram_business_manage_insights. No Facebook Page is needed. Or connect through the Facebook Page the account is linked to, using the Instagram API with Facebook Login and the matching instagram_* permissions. Either way, dmswift never sees your password and never drives a logged-in session. Step by step, with fixes for the usual snags: How to connect Instagram to dmswift. ## Limits worth knowing - The account must be a professional (business or creator) account. A linked Facebook Page is optional: it is only needed to connect through the Page. - Private messages are capped at 1,000 characters and carry no buttons; links travel in the text. - Liking a comment is not available on Instagram's API; replying, hiding and deleting are. Q: Do I need a business account? A: You need an Instagram professional account, business or creator. Switching is free in Instagram's settings. A Facebook Page is not required: Connect Instagram directly signs in with Instagram Login. Q: Is this allowed by Instagram? A: Yes. Everything goes through Meta's official Instagram API with permissions you grant and can revoke at any time. Nothing logs in as you. Q: Can my agent post a reel? A: Yes. With posts:write and posts:publish, an agent uploads the video, creates the reel with ig_type reel, and publishes or schedules it, within its daily publish cap. --- Source: https://dmswift.com/features/post-scheduler # Plan the week once. Facebook and Instagram post on time. One composer for both platforms. Draft it, schedule it or publish it now, attach a comment campaign so the post answers its own comments, and let your agent do the same over MCP. ## Every format - Text and link posts | Yes, with colored backgrounds | No (Instagram needs media) - Photo and video | Yes | Feed post, one item - Carousel | Multiple photos | 2 to 20 items - Reel | Video post | Yes, with a cover and an optional share to the feed - Story | No | Yes - First comment | Yes, text and image | Yes ## The lifecycle A post is a draft until you schedule or publish it; creating one never puts anything live. Facebook schedules are held by Meta; Instagram schedules are run by dmswift's own worker. Published posts keep their permalink and any error Meta returned, and any post can be duplicated as a new draft. ## From your agent, with a brake "Schedule the carousel for Thursday at 9 and put a campaign on it" is three tool calls: request_upload_link (the agent sends the images it made straight to dmswift, with no public link or repo; for words on a background it calls render_text_card and dmswift draws the slides itself), create_post with a schedule_at, and a campaign attached to it. Publishing is a separate scope from drafting, a key can be set to ask you before anything is published, and every key is capped at 15 publishes a day by default. Q: How many posts can I publish? A: Starter 50 a month, Pro 200, Business 600 and Agency unlimited under fair use. Facebook and Instagram posts count the same. A post counts in the month dmswift publishes it; drafts, uploads and scheduling ahead are never limited, and posts you publish directly in Facebook or Instagram don't count. Q: Can it post to Facebook Groups? A: No. Pages and Instagram professional accounts only. Q: Can I see the posts I made outside dmswift? A: Yes. The live posts list reads recent posts and reels straight from Meta, including ones created elsewhere, so a campaign can be attached to any of them. --- Source: https://dmswift.com/features/team-agency # Your team in the inbox, your clients under your brand. Seats come with the plan, never as a per-person add-on. On the Agency plan, each client gets its own workspace and sees your brand, not ours. ## Roles Invites go by email and expire after seven days. A waiting invite holds its seat until it is accepted, canceled or expires. - Viewer | Read the inbox, contacts, campaigns and reports | Change anything - Agent | Answer the inbox, take conversations over, tag contacts | Edit campaigns, channels or settings - Admin | Run campaigns, posts, moderation and settings | Manage the team or billing, or delete the account - Owner | Everything, including billing and the team | - ## Client workspaces The Agency plan runs up to 25 client workspaces and 30 channels from one account. Each workspace has its own channels, campaigns, contacts and inbox, and you switch between them from the dashboard. ## White-label - Your product name everywhere in the dashboard and in the emails your clients receive. - Your logo on the dashboard and on the sign-in screen, uploaded as a file. - Your accent color on buttons, links and highlights. - Your own domain: point a CNAME at us, and the sign-in screen there shows your brand before anyone signs in. - Your support address, so clients write to you. ## Agents per client API and MCP keys can be pinned to specific channels, so an agent built for one client is refused on every other client's channels. Q: What does a seat cost? A: Nothing on top of the plan. A seat is a person you invite besides yourself: Starter includes 1 (2 people with you), Pro 3, Business 10 and Agency unlimited. Q: Can a team member see billing? A: No. Billing, team management and deleting the account stay with the owner alone. Q: Is access set per channel? A: Team access is per account: a member reaches all of its channels or none. For per-channel limits, use pinned API keys for agents. --- Source: https://dmswift.com/features/unified-inbox # One inbox for everyone who wrote to you first. Instagram and Messenger arrive in the same list, one thread per person, with the automation's replies and your own in the same transcript. ## Built around people who wrote first The inbox holds conversations people started: a message, a story reply, a mention, or a private reply that answered their comment. Every thread shows how much of Meta's 24-hour reply window is left, because outside it Meta accepts nothing. ## Human takeover Take a thread over and every automation goes quiet for that contact until you hand it back. The person never gets a keyword answer in the middle of a conversation with you, and neither does your AI agent: an agent reply is refused while a human holds the thread. ## For a team - An agent seat answers the inbox and tags contacts; a viewer reads; an admin runs everything else. - The contact card sits beside the thread: tags, custom fields, notes, and where the person came from. - Every reply is attributed in the audit log. ## From your AI agent With inbox:read an agent lists and reads conversations; with inbox:write it can reply inside the window, up to 100 messages a day per key. A key in "ask me first" approval mode cannot send private messages at all. Q: Can I start a conversation from the inbox? A: No. Meta only allows messages to people who wrote to you, so the inbox only holds conversations they started. Q: Does it show WhatsApp? A: No. The inbox covers Instagram and Facebook Messenger. A WhatsApp agent can drive dmswift over MCP, but WhatsApp conversations are not part of this inbox. Q: Is there a mobile app? A: The dashboard works in a phone's browser and can send web push notifications for new conversations. --- Source: https://dmswift.com/features # Everything a Page and an Instagram account need, in one place. Each part works from the dashboard, from the REST API and from your AI agent over MCP. Each one also states its limits, because Meta's rules are part of the product. ## The same rules everywhere - Private messages only go to people who wrote first: a comment, a message, a story reply or a mention. - Free-form replies stay inside Meta's 24-hour window. Nothing here attaches a message tag to get around it. - Follow-ups and sequences only reach people who opted in or wrote first, and opt-outs are re-checked before every send. - Every action goes through Meta's Graph API with permissions you granted and can revoke from Facebook at any time. --- Source: https://dmswift.com/glossary # The words this product uses, defined once. Meta's messaging rules and the agent world both come with their own vocabulary. These are the terms you will meet on this site and in the dashboard. - MCP (Model Context Protocol): An open protocol that lets an AI agent discover and call tools on a server. dmswift's MCP server, at https://app.dmswift.com/mcp, exposes your Facebook Page and Instagram account as tools. See MCP server. - Agent action: One write call made with an API or MCP key, such as publishing a post or replying to a comment. Plans include a monthly number of them; reads never count. - Comment to DM: Answering a comment with a private message to the person who wrote it, usually alongside a public reply. See Comment to DM. - Private reply: Meta's mechanism for sending one private message in answer to a comment, within seven days of it. It is how comment-to-DM works on the official API. - 24-hour window: Meta allows a free-form message to a person only within 24 hours of their last message to you. Outside it, nothing is sent. - Human takeover: Taking a conversation over in the inbox. Automation and agent replies pause for that contact until it is handed back. - Campaign: An automation rule on a channel: a trigger (comment, DM, story reply or mention), keywords, and the public reply and private message it sends. - Channel: A connected Facebook Page or Instagram professional account. Plans are sized by how many channels they include. - Scope: A permission on an API key or OAuth connection, such as posts:publish or inbox:write. An agent only sees the tools its scopes allow. - Approval mode: A setting on an agent connection, "Ask me before anything is published", that holds every publish for the owner's approval. - Daily cap: A per-connection daily limit on publishes, comments and private messages (15, 30 and 100 by default) that stops a runaway agent. - OAuth 2.1: The sign-in standard Claude, ChatGPT and other MCP clients use to connect to dmswift without you pasting a key. - Instagram professional account: A business or creator Instagram account. Only these can be used through Meta's API. They connect directly with Instagram Login, or through a linked Facebook Page. - Merchant of Record: The company that legally sells to you and handles payment, tax and invoices. For dmswift that is Dodo Payments. --- Source: https://dmswift.com/guides/connect-facebook-page # How to connect a Facebook Page to dmswift. dmswift connects to your Page through Facebook Login, Meta's own sign-in window. You approve the permissions there once; dmswift never sees your Facebook password, and you can remove its access from Facebook at any time. ## Connect the Page - In dmswift, open Settings, then Channels, and choose Connect Facebook. - Facebook's window opens. Log in with the Facebook profile that manages the Page, if you are not logged in already. - If your Instagram account is linked to the Page, select it on the step that lists Instagram accounts. Instagram guide - Select the Page or Pages you want dmswift to manage. - Allow every permission on the list, then choose Save. You are back in dmswift with the Page connected. You need full control of the Page. A role with partial access cannot hand the Page to an app. ## Which permissions, and why Facebook lists each permission by its technical name. This is what each one lets dmswift do, and nothing more: dmswift messages only people who wrote to you first, inside Meta's 24-hour window, whatever the permissions would technically allow. - pages_show_list | See the list of Pages you manage, so you can choose which to connect. - pages_manage_metadata | Subscribe the Page to Meta's webhooks, so new comments and messages reach dmswift as they happen. - pages_read_engagement | Read your posts and their basic engagement, to attach campaigns and show results. - pages_read_user_content | Read the comments people leave on your posts, so a campaign can answer the ones that match. - pages_manage_engagement | Reply to, hide, delete and like comments as the Page. - pages_messaging | Send and receive Messenger messages for the Page: the private reply to a commenter and inbox replies to people who wrote first. - pages_manage_posts | Publish and schedule posts on the Page. - read_insights | Read the Page's statistics: reach, views and followers. - business_management | Reach Pages that belong to a Business portfolio you work in. - instagram_basic | Find the Instagram account linked to the Page and read its profile and posts. - instagram_manage_comments | Read Instagram comments, reply publicly, hide and delete them. - instagram_manage_messages | Send and receive Instagram direct messages. - instagram_content_publish | Publish feed posts, carousels, reels and stories on Instagram. - instagram_manage_insights | Read Instagram account and post statistics. ## Pages in a Business portfolio If the Page belongs to a Business portfolio (formerly Business Manager), its admin must give your Facebook profile full control of the Page: in Meta Business Suite, open Settings, then Pages, choose the Page, and assign yourself with full control. Then connect again. In Facebook's window, the Page now appears in the list. ## Reconnect when access expires Facebook ends an app's access when you change your Facebook password, when a Page admin role changes, when you remove dmswift in Facebook's settings, or when Meta asks for a fresh approval. dmswift then marks the Page Reconnect and tells you on the dashboard. Your campaigns, contacts and history stay where they are; anything that was waiting on the broken connection starts again. - Open Settings, then Channels. - Choose Reconnect next to the Page. - Go through Facebook's window again, keeping the same Page (and Instagram account) selected. Q: My Page is not in Facebook's list. A: Either your profile does not have full control of the Page, or the Page belongs to a Business portfolio that has not assigned it to you. Ask a Page or portfolio admin for full control, then choose Connect Facebook again. Q: I unticked a permission. Does that matter? A: Yes: the feature that needs it stops working, and dmswift marks the Page Reconnect. Choose Reconnect and allow the full list. Q: Facebook shows an "Invalid Scopes" warning. A: It is shown only to developers of a Meta app and is harmless for customers. If you are the app's owner, add the permission to the app's use cases in the Meta developer dashboard. Q: A banner says the Page needs to be reconnected. A: Its access ended, or it was connected before a permission dmswift now uses was added. Open Settings, then Channels, choose Reconnect and approve the permissions again. Q: Can I connect a Facebook Group? A: No. Meta's API does not let apps reply to or message from Group comments, so dmswift works with Pages only. Q: How do I remove dmswift's access? A: Disconnect the Page in Settings, Channels, or remove dmswift in Facebook's Settings, under Business integrations. Either one ends the access at once. --- Source: https://dmswift.com/guides/connect-instagram # How to connect Instagram to dmswift. There are two ways in. The quickest is Connect Instagram directly: you sign in to Instagram in Instagram's own window (Meta's Instagram API with Instagram Login), and no Facebook Page is needed. If your account is already linked to a Facebook Page, you can connect it through that Page instead. Either way it takes a few minutes. ## 1. Make the Instagram account professional Meta only lets tools reach Business and Creator accounts, on either route. Either type works with dmswift. - Open the Instagram app and go to your profile. - Open the menu, then Settings and activity. - Choose Account type and tools, then Switch to professional account. - Pick Business or Creator and finish the short setup. ## 2. Quickest: connect Instagram directly No Facebook Page needed. dmswift never sees your Instagram password. - In dmswift, open Settings, then Channels, and choose Connect Instagram directly. - Sign in to Instagram in Instagram's window and allow the permissions it lists: instagram_business_basic, instagram_business_manage_comments, instagram_business_manage_messages, instagram_business_content_publish and instagram_business_manage_insights. - You are sent back to dmswift with the account connected. Turn on message access in Instagram (see the last question below), so DMs reach dmswift. That is the whole setup on this route. Steps 3 to 5 are only for connecting through a Facebook Page. ## 3. Or: link it to a Facebook Page you admin You must be an admin of the Page, with full control. Use either route: - From Instagram or Facebook: open Accounts Center, then Accounts, then Add accounts, and add the Facebook profile that manages the Page. - Or from the Facebook Page: switch into the Page, open Settings, then Linked accounts, then Instagram, and choose Connect. ## 4. Connect Facebook in dmswift - In dmswift, open Settings, then Channels, and choose Connect Facebook (or Reconnect if the Page is already there). - In Facebook's window, on the step that lists Instagram accounts, select your Instagram account. This is the step people miss most often. - Select the Facebook Page it is linked to. - Allow every permission on the list. Each one is explained in the Facebook Page guide. If you skipped the Instagram step, nothing is broken: choose Reconnect and tick the account this time. ## 5. Pick the Instagram account - Back in dmswift, choose Connect via Facebook Page. - Pick the account from the list. It shows which Page each account is linked to. - Turn on message access in Instagram (see the last question below), so comments and DMs reach dmswift. ## Before you start - An Instagram professional account (Business or Creator). - Only for the Page route: a Facebook Page where you have full control. - A dmswift account. Every plan, and the 7-day trial, holds at least 2 channels, so the Page and Instagram fit together. See pricing. Q: dmswift says "No Instagram account found". What now? A: That message comes from the Facebook Page route. The simplest fix is to choose Connect Instagram directly (step 2), which needs no Page. On the Page route, the cause is one of three things: the account is not professional yet (step 1); it is not linked to a Page you connected to dmswift (step 3); or it was not selected in Facebook's window. For the last one, choose Reconnect in Settings, Channels, and tick the Instagram account on the step that lists them. Q: Do I need a Facebook Page to connect Instagram? A: No. Connect Instagram directly signs in with Instagram Login and needs only an Instagram professional account. Connecting through a linked Facebook Page is the other option, useful when you already manage the Page in dmswift. Q: Facebook's window shows an "Invalid Scopes" warning. A: That warning is shown only to the developers of a Meta app, and it is harmless for customers: carry on. If you are the app's owner, add the missing permission to the app's use cases in the Meta developer dashboard. Q: My Instagram is a personal account. A: Switch it to professional first (step 1). It takes a minute, it is free, and you can choose Creator if Business does not fit. Q: The Page belongs to another Business portfolio. A: Ask an admin of that Business portfolio to give you full control of the Page, then connect again. Without full control, Facebook will not hand the Page to dmswift. Q: Comments or DMs are not arriving. A: Reconnect the Page in Settings, Channels, so the Instagram account is subscribed to Meta's webhooks again. Messages from people who have never written to you before land in Instagram's Requests folder, not the main inbox. Q: Where is "Allow access to messages"? A: It must be on for DMs to reach dmswift. In the Instagram app: Settings, then Messages and story replies, then Message controls, then Connected tools, then turn on Allow access to messages. --- Source: https://dmswift.com/guides # Help guides, one task at a time. Each guide walks through one task in the order you will meet it, with the fixes for what usually goes wrong. Start with the Facebook Page, then Instagram. ## Looking for something else? - Connecting an AI agent such as Claude, ChatGPT or a WhatsApp agent: the MCP guides. - The API, MCP tools, scopes and errors: the reference. - Still stuck: write to support@dmswift.com and a person answers, usually within a business day. --- Source: https://dmswift.com/mcp/chatgpt # ChatGPT, with your Facebook Page and Instagram account. ChatGPT adds remote MCP servers as plugins. Add dmswift once, sign in, and ChatGPT, and your dot, can act on your channels within the caps you set. ## Set it up - In ChatGPT on the web, open Plugins (chatgpt.com/plugins), choose +, then Add custom MCP server. - Name it dmswift and paste https://app.dmswift.com/mcp as the Server URL. - Choose OAuth as the authentication. ChatGPT registers itself with dmswift automatically. - Accept the risk warning, then choose Create as a plugin. - Sign in to dmswift when asked, choose the channels, and decide on "Ask me before anything is published". - In a chat, type @ and pick dmswift, then ask. A custom MCP server is added on ChatGPT on the web, and workspace permissions apply in Business, Enterprise and Edu. ChatGPT asks you to confirm each write action by default. ## Your dot can use it too dots, OpenAI's personal agents, use the plugins installed and enabled for your ChatGPT account, with the permissions those plugins already have. Add dmswift as a custom MCP server in ChatGPT first, as above, and your dot can use it when you talk to it in ChatGPT, on a voice call, in Slack or in Microsoft Teams. dots are available on ChatGPT Pro plans (outside the EEA, the UK and Switzerland), Business Premium and Enterprise, where an admin turns them on. ## Try these - "What are the top unanswered questions under my posts this month?" - "Draft an Instagram carousel from these images for Friday 10am. Don't publish it yet." - "Reply to the three newest comments on my last Facebook post, thanking each person by name." ## Good to know MCP from $19/month: Starter includes full MCP and API access, with 300 agent actions a month, reads free and 1 key. Each write ChatGPT makes counts as one agent action. Replies under comments also count toward the connection's 30 comments a day, publishes toward its 15. Q: Do I need an API key for ChatGPT? A: No. ChatGPT signs in with OAuth and has no field for an API key or header. The token it receives is scoped, lasts an hour and is refreshed automatically. Q: Does my dot need its own setup? A: No. A dot uses the plugins already installed and enabled for your ChatGPT account, so adding dmswift in ChatGPT is the whole setup. Q: Can ChatGPT read my inbox? A: Yes, if you grant inbox:read. It can reply with inbox:write, inside Meta's 24-hour window and never while a person has taken the conversation over. --- Source: https://dmswift.com/mcp/claude-duet # Claude Duet, now on your Facebook Page and Instagram. Claude Duet is Anthropic's proactive agent app: you hand it a task, it keeps working after you step away, and it checks with you before it acts. Connect dmswift to your Claude account once and Duet can run your Page and Instagram too. ## Set it up (one connection for all of Claude) Claude Duet uses the connectors on your Claude account, so there is nothing separate to install. If dmswift is already connected in Claude, Duet has it from the first message. - In Claude on the web, desktop or phone, open Customize → Connectors → Add custom connector. - Name it dmswift and paste https://app.dmswift.com/mcp as the URL. - Choose Connect, sign in to dmswift and review the consent screen. - Pick all channels or one, and decide whether to tick "Ask me before anything is published". - Open Claude Duet and give it a task. Claude Duet is a research preview, open by waitlist on Claude Pro and Max in the US. Custom connectors depend on your Claude plan and, in an organization, on what its owner allows. ## Hand these to Duet - "My new reel goes live tonight. Set up a comment campaign on it: anyone who comments 'price' gets the price list in a DM. Test it first." - "Every morning, tell me which Instagram comments are still unanswered and draft replies for me to approve." - "Write three posts for next week from our latest blog post and schedule them on the Page and Instagram." - "How did last week's campaigns do? Which keyword brought the most DMs?" ## Why it fits a proactive agent dmswift turns comments into private messages and leads on Meta's official API, inside Meta's 24-hour messaging window. Duet decides what to do next; dmswift does it safely: daily caps (15 publishes, 30 comments and 100 private messages per connection by default), scopes per connection and an optional approval mode, so nothing goes public without you if you choose. ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. Every plan starts with a 7-day trial, no card. See pricing. Q: Does Claude Duet need its own dmswift connection? A: No. Duet uses the connectors already on your Claude account. Connect dmswift once in Claude (Customize → Connectors) and it works in Claude chats and in Duet. Q: Can Claude Duet answer Instagram and Facebook comments by itself? A: Yes, through dmswift: it can read comments, reply publicly, hide comments and run comment-to-DM campaigns. With "Ask me before anything is published" on, it asks you before anything goes public. Q: Can Duet send DMs to anyone? A: No. dmswift only messages people who wrote to you or commented on your post, inside Meta's 24-hour window, and never someone who opted out. Q: Who can use Claude Duet? A: Claude Duet is a research preview from Anthropic, open by waitlist on Claude Pro and Max in the US. dmswift itself works anywhere, with Claude, ChatGPT and other MCP apps. --- Source: https://dmswift.com/mcp/claude # Claude, with your Facebook Page and Instagram account. Add one custom connector and Claude can read your posts, schedule the next one, attach a comment campaign and tell you how it did, within the limits you set. ## Claude on the web, desktop or phone - Open Customize → Connectors → Add custom connector. - Name it dmswift and paste https://app.dmswift.com/mcp as the URL. - Choose Connect. Claude opens dmswift's sign-in; sign in and review the consent screen. - Pick all channels or one, and decide whether to tick "Ask me before anything is published". - Back in a chat, enable the dmswift connector from the tools menu and ask. Custom connectors depend on your Claude plan and, in an organization, on what its owner allows. ## Claude Code Then run /mcp inside Claude Code and choose dmswift to sign in. The same OAuth consent screen opens in your browser. ## Try these - "List my channels, then show me the last five posts on Instagram with their comment counts." - "Create a comment campaign on my newest reel: keyword 'price', reply publicly, DM the price list link. Test it first." - "Use the weekly_performance_report prompt for my Page." ## What Claude cannot do Claude cannot message people who never wrote to you, cannot send follow-ups to recent conversations, and cannot exceed the connection's daily caps of 15 publishes, 30 comments and 100 messages. It also cannot reach billing, the team or key management. Q: Which plan do I need? A: MCP from $19/month: Starter includes full MCP and API access, with 300 agent actions a month, reads free and 1 key. The 7-day Pro trial includes it too. Q: How do I disconnect Claude? A: On the dashboard's AI agents page, choose Disconnect next to the Claude connection. It stops working immediately. --- Source: https://dmswift.com/mcp/cursor # Your Page's content, next to your code. For developers and technical marketers: add dmswift to Cursor and its agent can read posts and comments, draft content and build campaigns while you work. ## With sign-in (recommended) Add this to ~/.cursor/mcp.json (or .cursor/mcp.json in a project). Cursor opens the sign-in the first time a tool is used. ## With an API key Create a key on the dashboard's AI agents page with only the scopes you want, and pass it as a header. A key is shown once; keep it out of version control. ## Useful from an editor - "Read the comments on my last five posts and write a FAQ section for our landing page from them." - "Create a draft post announcing the release in CHANGELOG.md, with a first comment linking to it." - "Dry-run the comment 'how much is it?' against my campaigns and tell me which one answers." Q: Which scopes should a development key have? A: Start with read scopes plus posts:write: the agent can draft but not publish. Add posts:publish when you trust the workflow. Q: Which plan do I need? A: MCP from $19/month: Starter includes full MCP and API access, with 300 agent actions a month, reads free and 1 key. Q: Is there a REST API too? A: Yes. Every MCP tool is a thin adapter over the same REST API, documented with an OpenAPI spec. --- Source: https://dmswift.com/mcp/gemini # Gemini, with your Facebook Page and Instagram account. The Gemini app connects custom apps through their MCP server. Add dmswift once on the web, sign in, and call it with @ in Gemini on the web or on your phone. ## Set it up - Open gemini.google.com on the web. Custom apps can only be added in the Gemini web app. - Go to Settings, then Connected Apps, then Custom apps, and choose Add a custom app. - Enter https://app.dmswift.com/mcp as the app's MCP server URL, choose Next and follow the prompts. - Sign in to dmswift when asked, choose the channels, and decide on "Ask me before anything is published". dmswift supports dynamic client registration, so you can leave Advanced features alone. - In a chat, type @, pick dmswift and ask. Once added, it also works in the Gemini app on Android and iOS. Google's requirements for custom apps: you are 18 or older, in the US, signed in with a personal Google Account (not work or school), with Keep Activity on, and using Gemini in English. ## Try these - "@dmswift What were my three best Instagram posts this month, and what do they have in common?" - "@dmswift Draft a Facebook post announcing Saturday's workshop for Friday at 6pm. Don't publish it yet." - "@dmswift Which questions under my posts this week got no answer?" ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. See pricing. Q: Can I add it from my phone? A: No. Google only lets you add custom apps in the Gemini web app. After that, dmswift is available in the Gemini mobile app too. Q: How do I disconnect Gemini? A: In Gemini, open the app's details and choose Disconnect, which revokes its permissions. You can also disconnect it on dmswift's AI agents page. Q: What about Gemini Enterprise? A: It is a separate product, where an admin adds MCP servers for the whole team. The steps on this page are for the Gemini app with a personal account. --- Source: https://dmswift.com/mcp/grok # Grok, with your Facebook Page and Instagram account. There are two ways in: a custom connector in Grok on grok.com, or Grok Bot, the always-on agent, which adds an MCP server when you ask it to. ## Grok on grok.com - Open grok.com/connectors, choose New Connector, then Custom. - Enter https://app.dmswift.com/mcp as the MCP server URL. - Complete the sign-in Grok asks for. When dmswift's consent screen opens, choose the channels and decide on "Ask me before anything is published". - Grok finds dmswift's tools. Ask in a chat. xAI documents custom connectors on grok.com for Business and Enterprise plans, where a team admin adds the server and each member then connects their own account. xAI does not document custom connectors on individual plans; there, use Grok Bot below. ## Grok Bot Grok Bot, built by SpaceXAI with Cursor, runs on desktop, iOS, Android and in Slack. Add dmswift by asking for it in a chat: The bot shows the server's details. Confirm, complete the sign-in Grok asks for, and the tools are available from your next message. On Teams and Enterprise plans, Grok Bot follows your team's connector policy. ## Try these - "Pull the comments on my last three Instagram reels and sort them into questions, praise and complaints." - "Create a comment campaign on my newest Facebook post: keyword 'menu', reply publicly, send the menu link privately. Test it first." - "Write my weekly report: reach, new contacts and the post that did best." ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. See pricing. Q: Does @grok on X work with dmswift? A: No. There is no documented way to add tools or MCP servers to the @grok bot on X. Use Grok on grok.com or Grok Bot. Q: Do I need an API key? A: No. You paste the URL and complete the sign-in Grok asks for. The connection gets only the channels and permissions you approve. Q: How do I disconnect Grok? A: On the dashboard's AI agents page, choose Disconnect next to the Grok connection. It stops working immediately. --- Source: https://dmswift.com/mcp/instinct # Message Instinct, and it does the work on your Page. Instinct is a hosted personal agent you talk to in your messages. Instinct publishes no MCP documentation, but we tested it: ask it to connect to dmswift, send it the link, and it works, with nothing to install. ## Set it up - In a chat with Instinct, ask it to connect to dmswift. - When Instinct asks for the MCP link, send it https://app.dmswift.com/mcp. - Complete the sign-in if Instinct asks for it: sign in to dmswift, pick the channels and decide on "Ask me before anything is published". - Message Instinct. Tested by dmswift on 19 October 2026. Instinct does not document MCP itself, so if its flow changes, the same link still applies: https://app.dmswift.com/mcp. ## Try these - "See my latest video? Create a comment campaign for it and send me the stats." - "Any comments on Instagram I haven't answered today? Reply to the questions and tell me what you said." - "Schedule this photo for Thursday at 9am on my Page and Instagram, with the caption I just sent." ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. See pricing. Q: Does Instinct officially support MCP? A: Instinct publishes no MCP documentation. dmswift tested the connection on 19 October 2026: Instinct asked for the MCP link, connected, and ran dmswift's tools. Q: Do I need an API key? A: No. You send Instinct the link and complete the sign-in it asks for. The connection gets only the channels and permissions you approve. Q: Is it safe to leave running? A: Each connection has daily caps (15 publishes, 30 comments and 100 private messages by default), scopes and an optional approval mode, and you can disconnect it on the AI agents page. --- Source: https://dmswift.com/mcp/n8n # Facebook and Instagram as tools in your n8n agent. n8n's AI Agent node can call any MCP server. Give it dmswift and a scoped API key, and the workflows you already run can post, answer and report on your channels. ## Set it up - In dmswift, open the AI agents page and create a key with the scopes the workflow needs. Pin it to one channel if the workflow is for one client. - In n8n, add an AI Agent node with the chat model you use. - Attach an MCP Client Tool to the agent. Set the endpoint to https://app.dmswift.com/mcp with the HTTP Streamable transport. - Choose header authentication and send Authorization: Bearer fbcb_… with your key. - Pick which dmswift tools the agent may use, or allow all that the key's scopes permit. ## Workflows people build - A Monday-morning schedule trigger that asks the agent for last week's report and emails it. - A form or a Google Sheet row that becomes a scheduled Instagram post with a campaign attached. - A chat trigger on your messaging app of choice, so you can talk to the agent from your phone. ## Events the other way dmswift can also push signed webhooks into n8n when something happens: a new contact, a matched comment, a delivered message. Use a Webhook trigger node and verify the signature as the docs describe. Q: Does n8n need OAuth? A: No. A server-side workflow uses an API key in a header. Keys can be revoked from the dashboard at any time. Q: Which plan do I need? A: MCP from $19/month: Starter includes full MCP and API access, with 300 agent actions a month, reads free and 1 key. Q: What happens if the workflow loops? A: The key's daily caps stop it: 15 publishes, 30 comments and 100 messages a day by default, with a daily_cap error that says when it resets. --- Source: https://dmswift.com/mcp/openclaw # OpenClaw, with your Facebook Page and Instagram account. OpenClaw is the open-source agent you host yourself and talk to on WhatsApp, Telegram, iMessage, Signal, Slack and more. Add dmswift as a remote MCP server and it can run your channels. ## Add the server Set the transport explicitly: when it is left out, OpenClaw assumes SSE for an HTTP server, and dmswift speaks Streamable HTTP. To start read-only, add --include 'list_*,get_*' and only the listing and reading tools are offered to the agent. ## Sign in with OAuth Set auth to oauth on the server's entry under mcp.servers in your OpenClaw config: Then run openclaw mcp login dmswift. Your browser opens dmswift's consent screen, where you pick the channels and decide on "Ask me before anything is published"; the token exchange is automatic. Check it with openclaw mcp status --verbose. ## Or use an API key Create a key on the dashboard's AI agents page and send it as a header, with the value read from an environment variable: OpenClaw redacts header values in its logs and status output. Static headers are ignored while auth is oauth, so use one or the other. openclaw mcp doctor dmswift --probe checks the connection. ## Try these - "See my latest video? Create a comment campaign for it and send me the stats tomorrow morning." - "Hide the spam under yesterday's reel and tell me what you hid." - "What did people ask under my posts this week that we never answered? Draft a campaign for the top one." ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. See pricing. Q: OpenClaw or Poke? A: Poke is hosted, so there is nothing to run. OpenClaw runs on your own machine or server, under the MIT license, and you choose its model and channels. Q: Which channels can I talk to it on? A: OpenClaw's docs list WhatsApp, Telegram, iMessage, Signal, Discord, Slack, Microsoft Teams, Google Chat, Matrix and more, many as plugins. Q: What if the agent loops? A: The connection's daily caps stop it: 15 publishes, 30 comments and 100 private messages a day by default, with a daily_cap error that says when it resets. --- Source: https://dmswift.com/mcp/perplexity # Perplexity, with your Facebook Page and Instagram account. Perplexity accepts custom remote MCP connectors. Add dmswift once, sign in, and Perplexity can act on your Page and Instagram within the limits you set. ## Set it up - In Perplexity on the web, open Plugins in the sidebar (or Customize, then Connectors). - Choose Add custom, then Connector, then Add MCP connector, then Remote. - Name it dmswift and enter https://app.dmswift.com/mcp as the MCP Server URL. - Set authentication to OAuth 2.0 and the transport to Streamable HTTP. Perplexity registers itself with dmswift automatically. - Accept the risk notice and choose Add. Then click the dmswift card to sign in, choose the channels, and turn it on. On Enterprise, an admin can add it for the whole organization under Enterprise settings, Permissions, Connectors permissions, + Custom connector. Members need an admin to allow custom connectors. ## Try these - "Compare my Instagram reach this month with last month and tell me which posts explain the difference." - "Find the questions people asked under my posts this week and add the answers I give you to the knowledge base." - "Schedule a Facebook post for Monday at 10am from this summary, and ask me before it goes live." ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. See pricing. Q: Does it work in Perplexity's Telegram or WhatsApp bots? A: No. Connectors live in Perplexity's own apps; its messaging bots cannot use them. Q: Does Perplexity need an API key from dmswift? A: No. With OAuth 2.0 you sign in to dmswift once, and the connection gets only the channels and permissions you approve. Q: How do I disconnect Perplexity? A: On the dashboard's AI agents page, choose Disconnect next to the Perplexity connection. It stops working immediately. --- Source: https://dmswift.com/mcp/poke # Text Poke, and it does the work on your Page. Poke is a hosted AI agent that lives in your messages. Add dmswift as an integration and you can run your Facebook Page and Instagram from a text, with nothing to install. ## Set it up - Open poke.com/integrations/new. - Name it dmswift and enter https://app.dmswift.com/mcp as the MCP Server URL. dmswift uses Streamable HTTP at that URL (there is no SSE endpoint), so choose Streamable HTTP if asked. - Leave API Key empty to sign in with dmswift: Poke runs OAuth by itself, because dmswift supports dynamic client registration. Or paste a dmswift API key, which Poke sends as Authorization: Bearer . - Choose Create Integration. If you left the key empty, sign in to dmswift, pick the channels and decide on "Ask me before anything is published". - Text Poke. ## From the command line Create the key on the dashboard's AI agents page with only the scopes Poke needs, and pin it to the channels you want it to touch. A key is shown once. ## Try these - "See my latest video? Create a comment campaign for it: anyone who comments 'link' gets the booking page. Send me the stats tomorrow." - "Any comments on Instagram I haven't answered today? Reply to the questions and tell me what you said." - "Schedule this photo for Thursday at 9am on my Page and Instagram, with the caption I just sent." ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. See pricing. Q: Which messaging apps does Poke work in? A: Poke works in Apple Messages (iMessage), SMS, RCS and Telegram, and in WhatsApp where it is available. Q: OAuth or an API key? A: Either. Sign-in is simplest and needs no copying. A key suits a setup link or the CLI, and you can limit its scopes and channels. Q: Is it safe to leave running? A: Each connection has daily caps (15 publishes, 30 comments and 100 private messages by default), scopes and an optional approval mode, and you can disconnect it on the AI agents page. --- Source: https://dmswift.com/mcp/whatsapp-agent # A studio owner, a phone, and a Page that runs itself. Dana runs a small studio. She does not open dashboards. She sends her AI agent a voice note on WhatsApp, and the agent does the work in dmswift. ## Tuesday, 8:40am ## Wednesday, 9:00am The numbers above are an illustration; the agent reports the real ones from get_stats and get_activity_log. Whether your agent takes voice notes depends on the agent; typing the same words works everywhere. ## Pick your route - Poke | Hosted, and the easiest: nothing to install. You text it on iMessage, SMS, RCS or Telegram, and on WhatsApp where it is available. | MCP: the URL, then dmswift sign-in or an API key - Instinct | Hosted, and just as easy: nothing to install. Tested by dmswift. | MCP: ask Instinct to connect to dmswift and send it the link https://app.dmswift.com/mcp - OpenClaw | Open source and self-hosted. WhatsApp, Telegram, iMessage, Signal, Slack and more. | MCP: the URL, with OAuth or an API key header - NanoClaw | Open source and self-hosted, each session in its own container. WhatsApp, Telegram, iMessage and more. | MCP: the URL, with the key held by its credential gateway - Muse (Meta) | Meta's personal agent, in its own app and in WhatsApp. US only. | Via API, not MCP: a Custom Connector built from our OpenAPI spec ## What is happening underneath - Dana's agent is connected to WhatsApp and holds a dmswift connection with the scopes she chose, pinned to her studio's channels. - It calls dmswift's tools: list_live_posts, create_campaign, test_campaign_match, hide_comment, then get_stats the next day. - dmswift does the Meta part on the official APIs: the public reply, the private message to each person who commented, the hidden spam. ## Connect NanoClaw The operator registers dmswift as a remote server (Streamable HTTP over HTTPS): NanoClaw keeps secrets out of its config: put your dmswift API key in its credential gateway (OneCLI Agent Vault by default), which adds it to requests. From a chat, the agent can also call its add_mcp_server tool; an admin approves the card and the container restarts with dmswift. ## Connect Muse (via API) - In dmswift, open the AI agents page and create an API key with only the scopes Muse needs. - In Muse, ask it to create a Custom Connector for dmswift from the API described at https://app.dmswift.com/openapi.json. - When Muse asks for credentials, enter the key on its credential screen, not in the chat. Muse keeps it in its Secure Credentials Store. - Ask Muse for what you need, in the app or in WhatsApp. This uses dmswift's REST API, not MCP. Meta does not review custom connectors, so give the key the narrowest scopes that work. ## What it will not do The agent cannot message anyone who did not write first, cannot send follow-ups to recent conversations, and stops at the key's daily caps. WhatsApp is only where Dana talks to her agent: dmswift does not read or send WhatsApp messages itself. MCP from $19/month, on Starter and up. Q: Does dmswift provide the WhatsApp agent? A: No. You bring your own agent connected to WhatsApp, iMessage or Telegram; dmswift is the server it uses for Facebook and Instagram. Q: Which route is easiest? A: Poke or Instinct: both are hosted. Add dmswift to Poke as an integration, or send Instinct the MCP link, and start texting. OpenClaw and NanoClaw suit people who want to run the agent themselves. Q: Can Meta AI in WhatsApp do this? A: No. Meta AI cannot add custom tools. Muse, Meta's separate personal agent, can reach dmswift through a Custom Connector built from our API. Q: Is this safe to leave running? A: Each connection has daily caps, scopes and an optional approval mode, and you can disconnect it from the dashboard in one click. --- Source: https://dmswift.com/mcp # The Facebook and Instagram MCP server your agent was missing. Connect dmswift to Claude, ChatGPT, Cursor, n8n or an agent you talk to on WhatsApp. It gets 44 tools for your Page and your Instagram account, and the limits you set. ## What your agent can do - Publish and schedule: Facebook posts, Instagram feed posts, carousels, reels and stories, with a first comment, now or at a set time. - Run comment campaigns: Find your latest post, attach a keyword campaign that replies publicly and sends a private message, test it, report. - Handle comments: Read comments, reply as the Page, hide or delete spam, like on Facebook. - Answer the inbox: Read conversations and reply inside Meta's 24-hour window, never while a person has taken over. - Report: The funnel, campaign stats, unanswered questions and Meta insights for channels and posts. - Know the business: Search and add to the knowledge base, so answers about prices and hours come from what you wrote. ## Connect in a minute The endpoint is https://app.dmswift.com/mcp. It speaks Streamable HTTP and signs in with OAuth 2.1, so clients that support sign-in need nothing but the URL. - Claude (web, desktop, mobile) | Customize → Connectors → Add custom connector, paste the URL, sign in. Guide - Claude Code | claude mcp add --transport http dmswift https://app.dmswift.com/mcp, then /mcp to sign in - ChatGPT | Plugins, Add custom MCP server, paste the URL, choose OAuth. Your dot can use it too. Guide - Cursor | Add the URL to ~/.cursor/mcp.json. Guide - n8n and scripts | An API key in an Authorization: Bearer header. Guide - A WhatsApp, iMessage or Telegram agent | Add the URL to Poke, OpenClaw or NanoClaw, or send it to Instinct. The story ## Works with your AI Every app in the first three groups below can add a remote MCP server by URL, according to its own documentation (Instinct by our own test). Paste https://app.dmswift.com/mcp and sign in, or send a dmswift API key as Authorization: Bearer where the app takes a header. Muse is the exception: it connects through the API, not MCP. - Chat apps: Claude · ChatGPT · Grok · Gemini · Perplexity · Le Chat · Microsoft Copilot Studio - Proactive and personal agents, including on WhatsApp, iMessage and Telegram: Claude Duet · Poke · Instinct · OpenClaw · Manus · Lindy · Hermes Agent · NanoClaw · Claude Code Channels - Builders and automation: Cursor · VS Code · Windsurf · Claude Code · n8n · Make · Zapier · Dify · Goose - Via API, not MCP: Muse (Meta), through a Custom Connector that Muse builds from our OpenAPI spec and an API key. Meta AI, the assistant built into WhatsApp, Instagram and Facebook, cannot add custom tools, so it cannot connect to dmswift. Instinct publishes no MCP documentation, but we tested it and it works: ask Instinct to connect to dmswift and send it the MCP link https://app.dmswift.com/mcp. Guide ## Every tool Only the tools a connection's scopes allow are registered, so the list your agent sees is exactly what it may do. Each tool carries MCP annotations: read-only, destructive, idempotent, and whether it reaches Meta. ## Safety, by design - Daily caps per connection: 15 publishes, 30 comments and 100 private messages a day by default. A spent cap stops the agent with a clear daily_cap error and the time it resets. - Approval mode: tick "Ask me before anything is published" when you connect, and every publish waits for your approval in the dashboard. Such a connection cannot schedule, comment or message. - Scopes: drafting (posts:write) and publishing (posts:publish) are separate. Moderation (comments:moderate) is separate from replying. A key can be pinned to specific channels. - Meta's rules, enforced: private messages only reach people who wrote first, inside the 24-hour window; follow-ups to recent conversations are not available to agents at all. - Off in one click: every connected app is listed on the dashboard's AI agents page, and disconnecting one ends it at once. ## Prompts that work - "See my latest video? Create a comment campaign for it: whoever comments 'guide' gets the link. Send me the stats tomorrow." - "What did people ask under my posts this week that we never answered? Draft a campaign for the top one." - "Schedule these three photos as an Instagram carousel for Thursday at 9am, with the caption I wrote, and ask me before it goes live." - "Hide the spam under yesterday's reel and tell me what you hid." - "Write my weekly report: three numbers that matter and two next steps." ## Plans MCP from $19/month. Starter includes full MCP and API access: 300 agent actions a month, reads free, 1 key. Pro ($49) includes 2,000, Business 10,000 and Agency 40,000. See pricing. Q: Do I need to write code? A: No. In Claude or ChatGPT you paste the URL and sign in. Code is only needed if you build your own agent. Q: Can the agent post without asking me? A: Only if you let it. Connections act on their own within their daily caps by default; tick "Ask me before anything is published" when you connect and every publish waits for your approval. Q: Which Instagram accounts work? A: Instagram professional accounts (business or creator). Connect one directly with Meta's Instagram API with Instagram Login, no Facebook Page needed, or through a Facebook Page it is linked to. Q: Is my data used to train models? A: dmswift does not train models. What your agent's provider does with the conversation is governed by that provider's terms. --- Source: https://dmswift.com/pricing # Priced by what you send, never by who you know. Four plans, and a 7-day Pro trial with no card. Annual billing gives you two months free. Contacts are unlimited on every plan, and your agent's reads are always free. ## Every limit, side by side Instagram publishing is included in every plan's post allowance: a reel, a story or a carousel counts as one post, exactly like a Facebook post. ## What each limit counts - Private message: one message dmswift sends for you - a campaign's reply to a comment or a message, a broadcast or sequence message to a recent conversation, a growth-link greeting, or the public answer to an Instagram mention. Replies you type yourself in the inbox don't count, and neither does the message sent when someone taps a button in a reply they already got. - Campaign: an automation you have saved, on any channel, switched on or paused. Starter holds up to 10 at a time; delete one to free its slot. From Pro up there is no limit. - Post: a post, reel, story or carousel that dmswift publishes on Facebook or Instagram, counted in the month it goes live. Writing drafts, uploading media and scheduling ahead are never limited, and posts you publish directly in Facebook or Instagram don't count (a post you hand to Facebook's own scheduler holds its slot from the moment you schedule it). If the month's posts are used up, a scheduled post waits and goes out when the allowance renews or right after you upgrade. - Follow-up broadcast: one send-out to people in recent conversations, counted when it starts or is scheduled, however many people it reaches (each message it sends also counts as a private message). Drafts cost nothing. Starter has 2 a month, Pro 30, Business 100, Agency unlimited. - AI reply: a reply that dmswift's AI writes and sends for you. Not included on Starter; Pro and Business have 2,000 a month, Agency is unlimited. The trial includes a smaller allowance, and AI replies stop if you choose Starter after the trial. - Teammate: a person you invite into the account besides yourself, with a viewer, agent or admin role. The owner never takes a seat; an invite that is still waiting holds one. - Monthly allowances start again on the 1st of every month; during the trial, the trial's allowance covers the whole trial. ## What an agent action is An agent action is a write call made with an API or MCP key: publishing or scheduling a post, creating or changing a campaign, replying to a comment, hiding one, tagging a contact, replying in the inbox. Reading is free: listing posts, reading comments, statistics, insights and conversations never counts. Each connection also has its own daily caps, whatever the plan: by default 15 publishes, 30 comments and 100 private messages a day. They protect your accounts from an agent that loops, and you can lower them per key. ## How billing works Full terms: Terms of service, Acceptable use and Refund policy. - 7-day trial of Pro, with no card, once per business, Page and Instagram account. The trial has lower sending limits than paid Pro. When it ends you choose a plan; until you do, the account is paused: automations stop, and you can still sign in, view and export everything. - 14-day money-back guarantee on your first payment. Ask at support@dmswift.com. Refund policy. - Upgrades apply at once, prorated for the rest of the period. Downgrades apply at the end of the period, so you keep what you paid for. - A failed payment gets 7 days of grace with everything working, then the account is paused. Nothing is deleted. - Messages only go to people who wrote first. dmswift never messages anyone outside Meta's 24-hour window or after they opt out. See the Acceptable Use Policy. - Dodo Payments is the Merchant of Record. Dodo Payments processes the payment, adds sales tax or VAT where it applies, and issues the invoice. - Founding price: Pro at $39/mo, locked in while you stay subscribed. For the first 100 customers or until Dec 31, 2026. Q: Do contacts count toward my plan? A: No. Contacts, tags and segments are unlimited on every plan. The meters are channels, private messages, campaigns, posts, follow-up broadcasts, AI replies and agent actions. Q: Is there a free trial? A: Yes: 7 days of Pro with no card, once per business. There is no free plan: when the trial ends, the account is paused until you subscribe, and nothing is deleted. Q: Can I get a refund? A: Yes, on your first payment: ask within 14 days at support@dmswift.com, from the account's email address, and we refund it in full, once per customer. Renewals are not refunded, but you can cancel at any time. See the Refund policy. Q: What happens when I run out of messages? A: Sending stops and the dashboard names the allowance that ran out. You are never moved up a plan automatically. Q: Does the post limit stop me from uploading or scheduling? A: No. Drafts, uploads and scheduling are unlimited. A post counts only in the month dmswift publishes it on Facebook or Instagram, and posts you publish directly in those apps never count. Q: Is the campaign limit about active campaigns? A: It counts every campaign you have saved, active or paused, across all your channels. Starter holds up to 10 at a time, and deleting one frees its slot. Pro and up have no campaign limit. Q: Which currencies do you charge in? A: Prices are in USD. Dodo Payments, our Merchant of Record, handles the payment and adds tax where it applies. Q: Which plans include the MCP server? A: Every paid plan, from Starter with 300 agent actions a month and one key. The 7-day trial includes it too, with trial limits. --- Source: https://dmswift.com/security # What we protect, and how. dmswift holds access to your Facebook Page and Instagram account, so the controls around that access are the product too. This page lists them plainly. ## Your Meta access - Connection goes through Meta's own sign-in windows: Facebook Login for Pages, and Instagram Login (or Facebook Login, through a linked Page) for Instagram, with the permissions listed on the home page; nothing else is requested. - Page and Instagram access tokens are stored encrypted (AES-256-GCM) and are never shown in the dashboard or to an API key. - You can revoke dmswift from your Facebook or Instagram settings at any time, and Meta's deauthorization and data-deletion callbacks are honored automatically. - There is no browser automation and no stored Facebook password, ever. ## Accounts - Sign in with Google, or a password stored as a scrypt hash. - Team members get roles; billing, team management and account deletion stay with the owner. - An audit log records every account action, including those made by API keys and agents. ## AI agents and API keys - A key is weaker than the person who made it: only the scopes it was granted, optionally pinned to specific channels, and no access to account administration or other keys. - Daily caps per key: 15 publishes, 30 comments and 100 private messages by default. - Approval mode: a connection can be made to ask before anything is published. - OAuth 2.1 with PKCE (S256) for MCP clients; access tokens last an hour and refresh tokens rotate on every use, so a replayed one ends the connection. - Agents can never send follow-ups to recent conversations, and can only message people who wrote first, inside Meta's 24-hour window. ## Data in transit and at rest - HTTPS everywhere, with HSTS on this site. - Outbound webhooks are signed with HMAC-SHA256 over a timestamp and the raw body, so a receiver can reject forgeries and replays. - Rate limits per key, per user and per IP. - Nightly database backups, with an off-site copy where configured. - Where data is processed, and by whom, is listed on the subprocessors page. ## Deletion - Deleting your account is immediate and cancels the subscription. - When a channel is disconnected, its data is deleted automatically after 90 days. - Details of what is kept and for how long are in the privacy policy and on the data deletion page. ## Reporting a vulnerability Write to support@dmswift.com with "Security" in the subject, with enough detail to reproduce the issue. Please do not access other customers' data, and give us a reasonable time to fix the problem before you disclose it. We answer security reports within two business days. --- Source: https://dmswift.com/support # A person answers, usually within a business day. Write to support@dmswift.com from the email address on your account. Most questions are answered within one business day; Business and Agency plans get priority. ## Contact Business days are Sunday to Thursday, Israel time. We answer in English or Hebrew. dmswift never messages anyone outside Meta's 24-hour window or after they opt out. Businesses using it must follow our Acceptable Use Policy. - Product questions and bugs | support@dmswift.com | Within 1 business day; priority on Business and Agency - Billing and refunds | support@dmswift.com | Within 1 business day - Security reports | support@dmswift.com, subject "Security" | Within 2 business days - Privacy and data requests | privacy@dmswift.com | Within the time the law requires, usually far sooner - Accessibility | accessibility@dmswift.com | Within 2 business days - Reporting abuse: an unwanted message, a scam or a harmful link from a business using dmswift | abuse@dmswift.com | Within 5 business days; urgent cases sooner ## Help yourself first - Help guides: Connect Instagram and your Facebook Page step by step, with fixes for the common snags. - API and MCP docs: Authentication, scopes, every tool and route, errors and limits. - Connect your agent: Step-by-step guides for Claude, ChatGPT, Cursor, n8n and WhatsApp agents. - Pricing and billing: Plans, the trial, upgrades, downgrades and the money-back policy. - Data deletion: Delete your account, a channel, or your data as someone who messaged a business. ## Things that help us help you - The channel's name and platform, and roughly when the problem happened. - The campaign or post involved, and what you expected to happen. - For an API or MCP issue: the error's code and next_step, and the tool or route you called. Never send your API key. Q: Is there live chat? A: Not yet. Email reaches the person who builds the product, which is usually faster than a queue. Q: Where can I see if something is down? A: There is no public status page yet. If something looks down, write to support@dmswift.com and we will confirm what is happening and when it will be fixed. --- # API and MCP reference (API.md) Source: https://dmswift.com/docs # dmswift - API & MCP Run a Facebook Page and an Instagram professional account from your AI agent or your own code: publish and schedule posts on both, run comment campaigns that reply publicly and by private message, read and moderate comments, read insights and the inbox - plus a customer inbox on Meta's official APIs that only replies to people who wrote to you first. This document is written to be handed, as-is, to an LLM agent or a developer. Everything an agent needs to operate the system is here. - Base URL: `https://app.dmswift.com` - MCP endpoint: `POST https://app.dmswift.com/mcp` - add it as a custom connector; sign-in is OAuth 2.1 (§2) - Machine-readable spec: `GET https://app.dmswift.com/openapi.json` ("dmswift API") - reports API version **3.1.0** - This document: `GET https://app.dmswift.com/llms.txt` - MCP registry manifest: `server.json` in the repository, name `com.dmswift/dmswift` The MCP server identifies itself as `dmswift` version **3.1.0**. Use of this API is subject to https://dmswift.com/acceptable-use. Never message people who did not contact the business first. --- ## 1. Concepts **Channel** - a connected account, addressed by its Meta id. Two kinds exist: - a **Facebook Page** (`platform: "facebook"`), connected by OAuth; - an **Instagram professional account** (`platform: "instagram"`), which is discovered through and operated with a connected Facebook Page's token. It has its own id, its own campaigns and its own activity log, but it holds no credential of its own - reconnecting the parent Page re-authorizes it. Everything below says "Page" where the API parameter is literally `page`, because that is the parameter name and it takes either kind of id. Where a capability is Facebook-only, it says so. **Post** - content managed by this system, on a Facebook Page or an Instagram account. A post has a lifecycle: ``` draft ──────► scheduled ──────► published │ ▲ └────────── publish now ──────────┘ error / canceled ``` Creating a post never publishes it. Publishing is a separate, explicit step. On an Instagram channel the same routes take `ig_type` (`feed`, `carousel`, `reel`, `story`), a caption in `message`, and media in `media_ids` / `media_urls` - one item for feed, reel and story, 2-20 for a carousel - plus `cover_url` and `share_to_feed` (default true) for reels and an optional `first_comment`. Scheduling, publish, status and duplicate behave as they do on Facebook; Instagram schedules are run by dmswift's own worker. Instagram publishing counts against the plan's post allowance like Facebook. One difference: Meta's API cannot delete Instagram media, so deleting a published Instagram post removes it from dmswift only (see §3, Instagram publishing). **Campaign** - an automation rule attached to a channel. When its trigger fires, the campaign matches the incoming text against its keywords and responds: optionally a public reply, and a private message to the person. A campaign either applies to one post (`post_id` set) or to every post on the channel (`post_id` empty). **Trigger** - what starts a campaign. One of: | `trigger` | Fires on | Platforms | |---|---|---| | `comment` | a comment on a post or reel - the original behaviour, and the default | Facebook + Instagram | | `dm` | an inbound direct message matching the keywords | Facebook + Instagram | | `story_reply` | a reply to one of the account's stories | Instagram | | `mention` | the account was @mentioned in a story | Instagram | Campaigns written before triggers existed carry `''` and are treated as `comment`. A campaign whose trigger is not `comment` may not set `public_reply` or `post_id` - there is no comment to answer in public and no post to bind to - and the API rejects both rather than silently ignoring them. Quiet hours, daily caps, once-per-user, variants and personalization work identically for all four, because they are one table rather than four features. Comment campaigns are matched by the webhook's comment path; the other three are matched when an inbound message is recorded in the inbox. The two combine: create a post with a campaign attached, and the moment the post goes live the campaign starts answering its comments. **Account and plan** - every channel belongs to one account, and every account has a plan that caps how many channels, campaigns, monthly private messages, monthly published posts, monthly AI replies, monthly broadcast runs and team seats it gets, and whether AI and API access are included. Exceeding a cap is a `402` with `code: "plan_limit"` (see Errors) - never a transient failure. **Delegated access, and what it means for a key.** An account owner can invite other people, who then act on the owner's channels while every page, contact, post and quota stays with the owner. A key resolves to the human who created it, and it inherits that human's delegated access - so a key created by an invited member can name the inviting account's channels with `?page=`, and `GET /api/pages` returns them. If that is not what you want, pin the key (§2). Team management itself is never reachable with a key. **Meta app** - the Facebook app the automation runs through. An account can plug in its own (Settings → "Your Meta app"), which is how a business operates its own Pages under Meta's Standard Access without waiting on dmswift's App Review. Accounts that don't bring one use the platform app. Either way the webhook URL is the same; the per-account verify token is what tells them apart. --- ## 2. Authentication Every request carries an API key as a bearer token: ``` Authorization: Bearer fbcb_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Keys are created in the dashboard on the **AI agents** page. A key is shown once, at creation, and cannot be recovered afterwards. MCP clients that support sign-in never need one - see OAuth below. A key is deliberately weaker than the human account that created it: - it holds only the scopes it was granted; - it may be pinned to specific Pages, and is then refused on every other Page - and a pinned key must *name* a Page with `?page=`, because inheriting "the account's first Page" was a way past the pin (`code: "page_required"`); - it can never reach account administration, the Meta app credentials, or key management - a key cannot create another key; - it carries daily caps per kind: **15 publishes** (a schedule counts), **30 comments** (a first comment and a comment reply count) and **100 private messages** by default, per UTC day; - every write it makes counts as an **agent action** against the plan's monthly allowance; reads are free; - it can be revoked without disturbing the owner's dashboard sessions. `GET /api/keys/self` tells a key where it stands - its scopes, pinned channels, approval mode, what is left of each daily cap, the plan, and the monthly agent actions used - and is open to every key: ```json { "key": { "id": 12, "name": "Claude (OAuth)", "scopes": ["pages:read", "posts:read"], "page_ids": [], "approval": "auto", "oauth": true, "expires_at": "2026-10-20T13:00:00.000Z" }, "caps": { "publish": { "cap": 15, "used": 2, "remaining": 13, "resets_at": "2026-10-21T00:00:00.000Z" }, "comment": { "cap": 30, "used": 0, "remaining": 30, "resets_at": "…" }, "dm": { "cap": 100, "used": 0, "remaining": 100, "resets_at": "…" } }, "plan": { "code": "pro", "name": "Pro", "api": true }, "agent_actions": { "used": 41, "limit": 2000, "remaining": 1959 } } ``` `GET /api/billing/status` (any scope) adds the subscription view: plan, status, trial end, limits and usage. ### OAuth 2.1 (MCP clients) dmswift is its own OAuth 2.1 authorization server, following the MCP authorization specification, so Claude (web, desktop, mobile), Claude Code, ChatGPT developer-mode connectors, Cursor and any other compliant client connect with nothing but the URL `https://app.dmswift.com/mcp`: 1. `POST /mcp` without a token answers `401` with `WWW-Authenticate: Bearer resource_metadata="https://app.dmswift.com/.well-known/oauth-protected-resource"`. 2. `GET /.well-known/oauth-protected-resource` (also at `…/oauth-protected-resource/mcp`, RFC 9728) names the resource `https://app.dmswift.com/mcp`, the authorization server and `scopes_supported`. 3. `GET /.well-known/oauth-authorization-server` (RFC 8414) lists the endpoints. `code_challenge_methods_supported` is `["S256"]`, `token_endpoint_auth_methods_supported` is `["none"]` (public clients), and `authorization_response_iss_parameter_supported` is `true` - every authorization response carries `iss` (RFC 9207). 4. `POST /oauth/register` (RFC 7591, JSON) registers a public client. Redirect URIs must be `https`, or `http` on `localhost` / `127.0.0.1` (any port is accepted at authorization time for loopback, RFC 8252). Other URIs in the request are dropped; none acceptable is `400 invalid_redirect_uri`. **Or skip registration** with a Client ID Metadata Document (the metadata says `client_id_metadata_document_supported: true`; this is what Claude's default "Use Claude's published identity" sends): the `client_id` is the `https` URL of a JSON document the client publishes. dmswift fetches it when the authorization request arrives - `https` on port 443 only, with a path; public addresses only (private, loopback, link-local/metadata, CGNAT, ULA and IPv4-mapped forms are refused, and the connection is pinned to the address that was checked); no redirects; 5 s; at most 64 KB; served as `application/json`. The document's `client_id` must equal the URL exactly, it must be a public client (`token_endpoint_auth_method` `none` or absent, no secret), and its `redirect_uris` follow the registration rule above (unacceptable ones dropped, none left is refused); the request's `redirect_uri` must be one of them. A valid document is cached for its `Cache-Control: max-age` (5 minutes to 24 hours, 1 hour if absent), a failure for a minute, and fetches are rate-limited per host. The consent screen names such a client together with its host - *Claude · claude.ai* - so a look-alike name cannot pass for another app. `POST /oauth/token` checks the document again (from the cache): one withdrawn since is `401 invalid_client`. 5. `GET /oauth/authorize` with `response_type=code`, `client_id`, `redirect_uri`, `code_challenge`, `code_challenge_method=S256`, `state`, optional `scope` (space-separated scopes from §2; unknown names such as `offline_access` are ignored, none means all) and `resource` (must be this server's `/mcp` URL, else `invalid_target`). A malformed request (no S256 challenge, wrong `response_type`, foreign `resource`, unknown client or redirect) is shown as an error page and never redirected - open registration must not turn this origin into a redirector. The person signs in to the dashboard if needed and sees a consent screen in their language (one mobile-first screen, details collapsed) with the client's name, where it returns to, and the same limits an API key has: **channels** (all, the default, or any subset of Facebook Pages and Instagram accounts), **what it may do** (a preset - *Read only*, *Read + draft*, *Full*, the default - or single scopes under *Advanced*; always inside what the client asked for), **"Ask me before anything is published"** (off by default: the connection acts on its own within the daily caps; on, it is an approval-mode key - see §6, 202) and the **daily caps** (default 15 publishes, 30 comment replies, 100 messages). See *Connections and limits* below. 6. `POST /oauth/token` (`application/x-www-form-urlencoded`): - `grant_type=authorization_code` with `code`, `code_verifier`, `client_id` (and optionally `redirect_uri`, `resource`). Codes live 5 minutes and are single-use; a replayed code ends the connection it produced. - `grant_type=refresh_token` with `refresh_token`, `client_id`. **Refresh tokens rotate**: each use returns a new pair and the old refresh token stops working; presenting a rotated-away token again later ends the connection. - The answer: `{ "access_token": "fbcb_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "fbrt_…", "scope": "…" }`. Errors are RFC 6749 shaped: `invalid_grant`, `invalid_target`, `unsupported_grant_type`. 7. `POST /oauth/revoke` (RFC 7009) with `token` (either token) ends the connection; always `200`. **The access token is an API key** with the granted scopes, channel pin and approval mode, valid for an hour, so everything in this document applies to it unchanged - scopes, pinning, caps, the plan gate. Its secret rotates on refresh but the key itself stays the same, so caps and the agent-action count carry across refreshes. Connected apps are listed on the dashboard's **AI agents** page, where the owner can disconnect any of them at once. A key pinned to channels lists only those in `GET /api/pages` (and `list_pages`); naming any other channel is refused (`403 page_forbidden`), and a page-scoped route called without a channel is `400 page_required`. ### Connections and limits A connection made by sign-in is exactly as controllable as a pasted key, before and after it is made. **Presets** (computed over the scopes on offer - every scope for a key, the client's request for a connection): | Preset | Scopes | Approval | |---|---|---| | `read` | every `*:read` scope | unchanged | | `draft` | `*:read` + `posts:write`, `posts:publish`, `campaigns:write`, `contacts:write`, `kb:write` | forced `ask` - a publish parks for approval (202), scheduling, public replies and switching a campaign on are refused | | `full` | everything on offer | unchanged | Any other combination reads back as `custom`. ```http GET /api/keys → { keys: [Key], scopes: [..every scope], presets: { read: [..], draft: [..], full: [..] } } GET /api/keys/oauth-apps → { apps: [App] } PATCH /api/keys/:id { preset? | scopes?, page_ids?, approval?, daily_cap?, comment_cap?, dm_cap? } → { ok, key: KeyLimits } DELETE /api/keys/oauth-apps/:id → { ok } (ends the connection: refresh token dead, access token revoked) DELETE /api/keys/:id → { ok, purged: false } (?purge=1 removes the row: { ok, purged: true }) ``` Both lists show **live credentials only**: a deleted key (its row is kept, revoked, so what it did keeps its attribution) and the token of a connection that was disconnected or whose refresh token lapsed are gone from `GET /api/keys` and `GET /api/keys/oauth-apps`; the audit log keeps their history. Deleting an already-deleted key answers `{ ok: true, purged: false, already_revoked: true }` (no second audit entry); PATCH of one is `404 err.key_not_found`. Only live keys and live connections count toward the plan's `max_keys`. - `Key`: `{ id, name, prefix, scopes, page_ids, approval, daily_cap, comment_cap, dm_cap, active, expires_at, last_used_at, created_at, oauth, oauth_app_id, client_name, allowed_scopes, preset }` - `oauth` is true for the access token of a connection (`oauth_app_id` is its id in `/api/keys/oauth-apps`). - `App`: `{ id, key_id, name, client_name, redirect_host, scopes, allowed_scopes, preset, page_ids, approval, daily_cap, comment_cap, dm_cap, created_at, connected_at, last_used_at }` - `key_id` is what PATCH takes. - `KeyLimits`: `{ id, scopes, page_ids, approval, daily_cap, comment_cap, dm_cap, oauth, allowed_scopes, preset }`. PATCH sends only what changes. `preset` and `scopes` are mutually exclusive (`err.key_preset_invalid`); every scope must exist (`err.key_scope_unknown`) and, for a connection, be one its client asked for (`err.key_scope_not_requested`); at least one (`err.api_key_scope_required`). `page_ids` is an array of channels the account can reach, own or by delegation (`err.page_not_in_account`); `[]` means every channel. `approval` is `auto` or `ask` (`err.key_approval_invalid`). Caps are whole numbers 0…the plan's monthly agent actions, 1000 on an unlimited plan (`err.key_cap_out_of_range`; 0 = no daily cap). An empty body is `err.key_cap_required`. Session only (`403` with a key). Changes apply on the key's **next request** - nothing is cached. A refresh never widens a connection: it rotates only the secret, and a `scope` in the refresh request is ignored. ### Scopes | Scope | Grants | |---|---| | `pages:read` | List channels; check what a Page's token can do | | `posts:read` | Read posts and their status | | `posts:write` | Create, edit and delete posts; upload media | | `posts:publish` | Put a post live (or schedule it) on Facebook or Instagram; comment as the Page on Facebook | | `campaigns:read` | Read campaigns; dry-run comment matching | | `campaigns:write` | Create and edit campaigns | | `analytics:read` | Statistics, the activity log and the analytics reports | | `contacts:read` | Read the contact list, its tags and its stats | | `contacts:write` | Tag contacts | | `kb:read` | Read and search the business knowledge base | | `kb:write` | Add knowledge base entries | | `inbox:read` | Read inbox conversations (people who wrote first) | | `inbox:write` | Reply in the inbox - inside Meta's 24-hour window, never while a human took over, never for approval-mode keys | | `comments:read` | Read comments on live posts | | `comments:write` | Reply to comments (counts as a comment) and like them (Facebook only) | | `comments:moderate` | Hide, unhide and delete comments | | `insights:read` | Meta insights for channels and posts | `posts:write` and `posts:publish` are separate on purpose: drafting content and putting it in front of an audience are different levels of trust. A key that may draft but not publish is a safe default for a new agent. The read/write split is deliberately uneven on contacts and the knowledge base: a key can *add* knowledge and *tag* a contact, but cannot edit or delete either. Destructive operations on a customer's own records stay with a human. See §7 for the full picture. --- ## 3. MCP The MCP server speaks Streamable HTTP at `POST /mcp` and authenticates with an OAuth access token or an API key, both as a bearer token. It is stateless - each request is independent, no session id is needed. ### Supported AI clients MCP URL `https://app.dmswift.com/mcp`. Sign-in is OAuth 2.1 (with dynamic client registration); the alternative is an API key sent as `Authorization: Bearer `. MCP is included from the $19/month Starter plan. | Client | How | |---|---| | Claude (web, desktop, mobile) | Customize → Connectors → Add custom connector → the URL, then sign in (OAuth) | | Claude Code | `claude mcp add --transport http dmswift https://app.dmswift.com/mcp`, then `/mcp` → Authenticate (or add `--header "Authorization: Bearer "`) | | ChatGPT (and dots) | chatgpt.com/plugins → + → Add custom MCP server → the URL, authentication OAuth → Create as a plugin. ChatGPT has no API-key option. dots use the plugins you added | | Grok | grok.com/connectors → New Connector → Custom → the URL, then complete the sign-in Grok asks for (xAI documents custom connectors for Business/Enterprise plans on grok.com). Grok Bot accepts "Add this MCP server: " in chat | | Gemini | gemini.google.com → Settings → Connected Apps → Custom apps → Add a custom app → the URL (web only; US personal accounts) | | Perplexity | Plugins → Add custom → Connector → Add MCP connector → Remote → the URL; OAuth 2.0 or API Key | | Poke (iMessage, SMS, WhatsApp, Telegram) | poke.com/integrations/new → Name, MCP Server URL, API Key → Create Integration. dmswift uses Streamable HTTP at `https://app.dmswift.com/mcp` (no SSE endpoint): choose Streamable HTTP if asked | | Instinct | tested by dmswift (Instinct publishes no MCP docs): ask Instinct to connect to dmswift and send it the MCP link `https://app.dmswift.com/mcp` | | OpenClaw (WhatsApp, iMessage, Telegram, …) | `openclaw mcp add dmswift --url https://app.dmswift.com/mcp --transport streamable-http`, then a `headers` bearer key in the config, or `auth: "oauth"` + `openclaw mcp login dmswift` | | Cursor | `~/.cursor/mcp.json`: `{ "mcpServers": { "dmswift": { "url": "https://app.dmswift.com/mcp" } } }` | | VS Code (Copilot agent mode) | `.vscode/mcp.json`: `{ "servers": { "dmswift": { "type": "http", "url": "https://app.dmswift.com/mcp" } } }` | | n8n, Make, Zapier | the MCP Client node / module / app with the URL and an API key as a Bearer token | | Muse (Meta) | **via API, not MCP**: ask Muse to create a Custom Connector from `https://app.dmswift.com/openapi.json` and give it an API key | Not supported: Meta AI (it cannot add outside tools). ```json { "mcpServers": { "dmswift": { "type": "http", "url": "https://app.dmswift.com/mcp", "headers": { "Authorization": "Bearer fbcb_your_key_here" } } } } ``` OpenClaw (an agent people talk to on WhatsApp), in its config under `mcp.servers`: `"dmswift": { "url": "https://app.dmswift.com/mcp", "transport": "streamable-http", "headers": { "Authorization": "Bearer ${DMSWIFT_KEY}" } }` **Only the tools your key's scopes allow are registered.** If a tool is missing from `tools/list`, the key lacks its scope - the fix is a new key (or reconnecting), not a retry. Every tool carries MCP **annotations** - `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` (it reaches Meta or people outside dmswift) - and the list is always in the same order. Results are compact JSON text, and the same object as `structuredContent` when the result is an object. A failed call is `isError: true` with the API's error body plus **`next_step`**: one sentence saying what to do (stop and report a `daily_cap` until `resets_at`, tell the owner about a `plan_limit`, call `list_pages` after `page_forbidden`, …). The server also offers two **prompts** - `launch_comment_campaign_for_latest_post` and `weekly_performance_report` - and two **resources**: `dmswift://docs/agent-guide` (how to use this server) and `dmswift://pages` (the channels, with `pages:read`). If the account's plan does not include the API, `/mcp` answers every call with a JSON-RPC error `-32001` whose `data` is `{ "code": "plan_limit", "upgrade_url": "…" }` - readable by the model, so it can tell the owner. MCP is a thin adapter over this app's own REST API: every tool call goes back through `/api` carrying the caller's key, so a tool can never quietly outgrow what the same key could do over REST. A tool whose REST route is closed to keys therefore fails the same way REST does - see §7. ### Tools | Tool | Scope | What it does | |---|---|---| | `get_account_status` | - | Permissions, channels, approval mode, caps left today (`GET /api/keys/self`) and, under `billing`, plan, trial, monthly limits, usage and `upgrade_url` (`GET /api/billing/status`, without the price list) | | `list_pages` | `pages:read` | Channels this key may act on. **Call this first.** | | `check_page_permissions` | `pages:read` | Whether this channel can publish (`instagram_content_publish` on Instagram) | | `list_posts` | `posts:read` | Posts managed through this system, filterable by status (`draft`, `scheduled`, `published`, etc.); rows carry `platform` and the Instagram fields | | `list_live_posts` | `posts:read` | Recent live posts/reels directly from Facebook or Instagram (pass `include_scheduled: true` to include Facebook scheduled posts) | | `list_scheduled_posts` | `posts:read` | Scheduled posts both directly from Facebook (`/{page-id}/scheduled_posts`) and from dmswift (`local`); Instagram has only dmswift's queue | | `get_post` | `posts:read` | One post in full, including permalink, errors, and attached `media` (`media_id`, `signed_path`) | | `upload_media` | `posts:write` | Upload an image or video (via `data` base64 or `url`) and get back `media_id` (`file`), `signed_url`, `size` and `sha256`. Pass `expected_sha256` with base64. Instagram photos must be JPEG | | `render_text_card` | `posts:write` | dmswift draws a text-on-background JPEG (or up to 10 for a carousel) and returns `media_ids`. No image to make or upload; works from any sandbox | | `get_brand_kit` / `set_brand_kit` | `posts:read` / `posts:write` | The channel's default card look: preset or gradient, text color, footer, logo | | `create_post` | `posts:write` | Create a draft or a scheduled post on Facebook or Instagram: media (`media_ids` / `media_urls`), alt text, first comment, a colored background (Facebook), `ig_type` / `cover_url` / `share_to_feed` (Instagram), and an attached campaign | | `update_post` | `posts:write` | Edit an unpublished post; omitted fields (Instagram ones included) stay as they are | | `duplicate_post` | `posts:write` | Clone an existing post into a fresh draft (Instagram type and reel cover included) | | `delete_post` | `posts:write` | Delete, optionally from Facebook too. Instagram media cannot be deleted through the API: removed from dmswift only (`remote_deleted: false`) | | `publish_post` | `posts:publish` | Put a post live on Facebook or Instagram. Supports `dry_run`. | | `comment_on_post` | `posts:publish` | **Facebook only.** Comment as the Page on one of its own published posts (or reply to a comment thread on it), with text and/or an image (`media_id`, `media_data` base64, or `media_url`). On Instagram use `reply_to_comment`, or `first_comment` on `create_post` | | `list_comments` | `comments:read` | Comments on the channel's live posts (Facebook and Instagram), newest first, paged by `next_cursor` | | `reply_to_comment` | `comments:write` | Public reply under a comment, Facebook or Instagram (counts as a comment; refused for approval-mode keys) | | `like_comment` | `comments:write` | Like a comment - Facebook only | | `hide_comment` | `comments:moderate` | Hide or unhide a comment | | `delete_comment` | `comments:moderate` | Delete a comment permanently | | `list_conversations` | `inbox:read` | Inbox conversations people started, with `counts`; filter by `status` (`open` default, `done`, `snoozed`, `all`), `unread`, `q`; page with `offset` | | `get_conversation` | `inbox:read` | One conversation's messages plus `window_open` and `takeover_active`; older messages with `before` | | `reply_to_conversation` | `inbox:write` | Private reply inside Meta's 24-hour window, never while a human took over (counts as a dm) | | `get_insights` | `insights:read` | Channel reach, views, followers and engagement from Meta, with daily series | | `get_post_insights` | `insights:read` | Metrics for one live post or reel | | `list_campaigns` | `campaigns:read` | Campaigns with hit counts | | `get_campaign` | `campaigns:read` | One campaign in full | | `create_campaign` | `campaigns:write` | New comment, DM, story reply or mention automation rule | | `update_campaign` | `campaigns:write` | Partially update an existing campaign | | `delete_campaign` | `campaigns:write` | Delete a campaign | | `set_campaign_active` | `campaigns:write` | Pause or resume a campaign | | `test_campaign_match` | `campaigns:read` | Dry-run a comment against the campaigns | | `get_stats` | `analytics:read` | Totals for a channel | | `get_activity_log` | `analytics:read` | What the bot recently did, and whether it worked | | `get_analytics` | `analytics:read` | The funnel, headline metrics with period-over-period change, and `best_times` (peak hours/days to post) | | `get_unanswered_questions` | `analytics:read` | Comments that matched no campaign, deduplicated and ranked by how often they were asked. **Each one is a campaign worth creating** - the single most actionable report the product produces | | `list_contacts` | `contacts:read` | Search and filter the people who engaged: text, tags, platform, subscribed state, source, recency | | `tag_contact` | `contacts:write` | Add tags to one contact. Tags are how segments are built | | `search_knowledge` | `kb:read` | Retrieve what this business wrote about itself. Use before answering anything factual - prices, hours, policies - instead of guessing | | `add_knowledge` | `kb:write` | Record a fact about the business. One fact per entry, with a title someone would search for | The comment, inbox and insights tools sit on the routes in §4 ("Comments, insights and the inbox"). Recommended workflow: `list_pages` → `list_live_posts` → `create_campaign` (bound with `post_id`) → `test_campaign_match` → `get_stats`. An agent asked to "improve this page" should start with `get_unanswered_questions`, then `search_knowledge` to check whether the answer already exists, then `create_campaign`. Other workflows the server's instructions teach: - **Instagram post:** `upload_media` (JPEG) → `create_post` with `ig_type` and `media_ids` → show the draft → `publish_post` with `dry_run`, then for real. - **Text post (no photo):** `render_text_card` (`slides` for a carousel) → `create_post` with its `media_ids` → show the draft → `publish_post`. Only text crosses the connection. - **Comments:** `list_comments` → `reply_to_comment` for real questions (after `search_knowledge`), `hide_comment` for spam (reversible; preferred), `delete_comment` only when asked, `like_comment` on Facebook. - **Inbox:** `list_conversations` (`unread: true`) → `get_conversation` → `reply_to_conversation` while `window_open` and not `takeover_active`. - **Results:** `get_stats` and `get_activity_log` for campaigns; `get_insights` and `get_post_insights` for Meta's reach and views. Error results carry `next_step` for the cases an agent meets here too: `permission_missing` (reconnect the channel), `human_takeover`, `err.inbox_window_closed`, `err.ig_publish_limit`, `err.publish_interrupted` (never retry) and `err.posts_facebook_only`. ### Worked example Publishing a post that answers its own comments: 1. `list_pages` → take the `id` of the channel you want. 2. `create_post` with the Page id, the text, optional `first_comment` or `bg_preset`, and a `campaign`: ```json { "page": "1247943345074815", "message": "We just launched a new course! Want details - comment 'price' below", "first_comment": "Full syllabus and registration: https://example.com/course", "campaign": { "name": "Price inquiries", "keywords": "price,cost,details", "public_reply": "We sent you a DM 🙂", "dm_text": "Hi {name}, here are the course details...", "button_title": "Course syllabus", "button_url": "https://example.com/course", "like_comment": true } } ``` 3. `test_campaign_match` with `text: "how much does it cost?"` → confirm the campaign matches before anyone sees the post. 4. `publish_post` with `dry_run: true` → confirm it is publishable. 5. `publish_post` → live. The campaign now answers comments on it automatically, and `first_comment` is posted under the story. ### Colored backgrounds, first comment & commenting with images - `bg_preset`: Facebook colored-background preset ID (`text_format_preset_id`), supported on text-only posts up to 130 characters (no link or media). Valid preset IDs: `1881421442117417` (black), `106018623298955` (purple), `1903718606535395` (red), `219266485227663` (magenta), `1289741387813798` (dark red), `249307305544279` (red-blue gradient), `200521337465306` (fire), `1679248482160767` (blue-white gradient), `127541261450947` (soccer green), `143093446467972` (blue sky), `931584293685988` (aqua gradient), `303063890126415` (orange-purple gradient). - `first_comment`, `first_comment_media_id`, `first_comment_media_data` & `first_comment_media_url`: Up to 8,000 characters and/or an image posted by the Page as the first comment right after the post goes live (ideal for placing external URLs or infographics in the comments without reducing organic feed reach). - `comment_on_post`: Post an additional comment as the Page on any published post at any time - pass `post_id` (or `fb_object_id` for a live Facebook post or comment thread), `message`, and/or an image via: - `media_id`: the `file` / `media_id` of an image already uploaded to dmswift (e.g. `"a1b2c3d4...png"` or `"/api/uploads/a1b2c3d4...png"` from `upload_media`, `POST /api/uploads`, or `get_post`), - `media_data`: base64-encoded image bytes (or `data:image/png;base64,...`) + optional `media_mime`, - `media_url`: a public URL, a signed dmswift upload URL (`/api/uploads/?sig=...`), or an `/api/uploads/` URL (which is resolved directly from disk without requiring a second download). ### Merge tokens These are replaced in `dm_text`, `public_reply` and `postback_text` before sending. Longer tokens are substituted first, so a short token never partially consumes a longer one. | Token | Also written as | Becomes | |---|---|---| | `{name}` | `{first_name}`, `{שם}` | the person's first name | | `{last_name}` | `{שם_משפחה}` | the surname (empty when only one name is known) | | `{full_name}` | `{שם_מלא}` | the whole name as the platform reports it | The Hebrew spellings exist because tenants writing Hebrew campaign copy already type them; they are template syntax, not translated UI copy. On Facebook, Messenger exposes only a first name without extra permissions. On Instagram the profile gives a username where Messenger gives a name, so `{name}` falls back to the username. A missing name never fails a send - it yields an un-personalized but delivered message. Newline-separated `public_reply` variants keep repeated replies from looking automated. The variant is chosen by hashing the comment id, not at random: different commenters get different lines, and a retry of the same comment picks the same line it would have sent the first time. Set `dm_variants: 1` to do the same with `dm_text`. ### Scheduling Pass `schedule_at` to `create_post` as a unix timestamp at least 10 minutes in the future. On Facebook, scheduling is handed to Facebook itself, so the post goes out at the appointed time even if this server is down. Instagram has no native scheduler: its schedules are run by dmswift's own worker. ### Media Pass `media_urls` - a list of image or video URLs (and optional `media_alts` for image accessibility alt text). The server downloads and attaches them. Allowed: JPG, PNG, GIF, WEBP, MP4, MOV, up to 25MB each, 10 per post on Facebook (Instagram: see below). A link preview and attached media are mutually exclusive; media wins. URLs must be publicly reachable. Addresses that resolve inside a private network are refused, and redirects are not followed. ### Instagram publishing `create_post`, `update_post`, `publish_post`, `duplicate_post` and the list tools take an Instagram channel's id exactly as they take a Facebook Page's. What changes is the content rules, enforced on create and on every edit: | `ig_type` | Media | Notes | |---|---|---| | `feed` (default) | exactly 1 photo | a video is refused (`err.ig_feed_video`) - publish it as a `reel` | | `carousel` | 2-20 photos and/or videos | in `media_ids` / `media_urls` order | | `reel` | 1 video (MP4/MOV) | optional `cover_url` (an uploaded JPEG or a public https URL) and `share_to_feed` (default true) | | `story` | 1 photo or video | no first comment | - Photos must be **JPEG**, up to 8 MB (`err.ig_jpeg_only`, `err.ig_image_too_large`). - The caption is `message`: up to 2,200 characters, 30 hashtags, 20 @mentions. - No `link` (`err.ig_no_link` - put it in the caption or the first comment) and no `bg_preset`. `first_comment` is text only, up to 2,200 characters. - An edit that omits `ig_type`, `cover_url` or `share_to_feed` keeps the stored values. - Publishing: dmswift checks Meta's 24-hour API publishing quota first (`400 err.ig_publish_limit`, nothing is created), creates the media containers, waits for Meta to process them (a reel can take minutes), publishes, reads the permalink and posts the first comment. A container Meta rejects fails the post; `err.publish_interrupted` means the outcome is unknown and is **never** retried - check `get_post` or `list_live_posts`. A scheduled post that meets a full quota is pushed back an hour. - Deleting: Meta's API cannot delete Instagram media. `delete_post` with `also_remote` removes the post from dmswift and answers `{ ok: true, remote_deleted: false, note, note_code }`; it stays on Instagram until the owner deletes it in the app. - Comments under Instagram posts are read and answered with the comment tools (`list_comments`, `reply_to_comment`, `hide_comment`, `delete_comment`); `comment_on_post` and `like_comment` are Facebook-only. An Instagram reel through MCP: ```json { "page": "17840000000000000", "ig_type": "reel", "message": "New drop. Comment LINK for the shop", "media_ids": ["a1b2c3d4e5f6a1b2c3d4e5f6.mp4"], "share_to_feed": true, "campaign": { "name": "Reel link", "keywords": "link", "dm_text": "Here you go {name}", "button_title": "Shop", "button_url": "https://example.com/shop" } } ``` --- ## 4. REST API Everything the working MCP tools do is available directly. All page-scoped endpoints take the channel id as `?page=` (`?page_id=` is read the same way), and write endpoints also expect `page_id` in the body. ### Pages ```http GET /api/pages GET /api/pages?include=disconnected GET /api/page-scopes?page= ``` `GET /api/pages` returns each channel's `id`, `name`, `platform`, `username`, `avatar`, `parent_page_id` and `connected_at`. Tokens never leave the server. With `include=disconnected` the list also carries channels that were disconnected (kept so they can be reconnected), each with `disconnected: true`; connected channels never carry the field. A pinned key sees only its pages either way. ### Posts ```http GET /api/posts?page= GET /api/posts?page=&scheduled=1 GET /api/posts?page=&include_scheduled=1 GET /api/page-posts?page=&status=&limit= GET /api/page-posts/?page= POST /api/page-posts?page= PUT /api/page-posts/?page= DELETE /api/page-posts/?page=&remote=1 POST /api/page-posts//publish?page= POST /api/page-posts//comments?page= POST /api/page-posts//duplicate?page= POST /api/page-posts//status?page= ``` `GET /api/posts` (`posts:read`) is different from the rest: it reads **live content from Meta** rather than this system's own post rows - recent Page posts on Facebook, recent media and reels on Instagram, normalized into the same `{ id, message, permalink_url, created_time }` shape, plus `type` (`photo`, `video`, `reel`, `album`, `link` or `text`) and `comments_count` when Meta reports them (either may be absent; Instagram rows also keep `media_type` and `thumbnail`). It is how you find the id of an existing post to bind a campaign to. `GET /api/page-posts` reads posts this system manages. A row with `has_thumb: true` has a picture at `GET /api/posts//thumb?page=` (`posts:read`): the image bytes, fetched by the server from Meta's CDN (`*.fbcdn.net`, `*.cdninstagram.com` over https only, every redirect re-checked, 2 MB cap, raster types only) and sent with `Cache-Control: private, max-age=3600`. The post must be in that page's recent list; anything else is `404`, and a picture the CDN will not hand over is `502 err.thumb_unavailable`. Create body: ```json { "page_id": "1247943345074815", "message": "text", "link": "https://example.com", "media": [{ "file": "...", "mime": "image/png", "name": "a.png", "size": 1234 }], "scheduled_at": 1785000000, "fb_schedule": true, "auto_rule_template": { "name": "...", "keywords": "...", "dm_text": "..." } } ``` `status` is derived from `scheduled_at`, never accepted from the client. Instead of `media`, a body may carry `media_ids` (uploaded files) and/or `media_urls` (fetched first). On an Instagram channel the body also takes `ig_type`, `cover_url`, `share_to_feed` and `first_comment` under the rules in §3 ("Instagram publishing"); there `link` is refused and `bg_preset` / `fb_schedule` are ignored. ### Media upload ```http POST /api/uploads { "url": "https://example.com/image.jpg", "page_id": "" } # by URL { "data": "", "mime": "image/png", "name": "a.png", "page_id": "", "expected_sha256": "" } # by base64 GET /api/uploads/ GET /api/uploads/?sig= ``` Returns `{ ok: true, media: { file, media_id, name, mime, size, sha256, signed_path, signed_url } }`; `size` and `sha256` describe the decoded bytes that were stored. Pass `media_id` (or `media`) into `comment_on_post`, `create_post`, or `POST /api/page-posts//comments`. `signed_url` can also be fetched directly without an `Authorization` header. **Integrity of base64 uploads.** A model that sends a file as base64 emits it token by token, and one dropped character shifts every byte after it. Compute the SHA-256 (hex) of the file before encoding and send it as `expected_sha256`: if the decoded bytes hash differently the upload is refused with `400 err.checksum_mismatch` and nothing is stored (the error text carries the size and hash that arrived). A malformed `expected_sha256` is `400 err.invalid_checksum`. Independently of that, a JPEG without its end-of-image marker, or a PNG with a bad chunk CRC or no `IEND`, is refused with `400 err.invalid_image`. `expected_sha256` applies to `data` only, not to `url`. **The file type is read from the bytes.** `mime` is optional: a JPEG sent as `image/png`, or with no `mime`, is stored as a JPEG (Instagram needs JPEG). A `mime` outside the allowed list is refused, and so are bytes with no recognisable JPEG, PNG, GIF, WEBP, MP4 or MOV signature, whatever `mime` says (`400 err.unsupported_file_type`). HEIC/AVIF photos are refused with that reason; an upload link page converts them to JPEG. A phone "motion photo" (a JPEG with a video appended) is accepted. ### Upload links (the user's own photos) No public URL, repo or file hosting. An autonomous agent that made an image POSTs its bytes to the link's `upload_url`; an agent in a chat app, which sees a photo the person sent but cannot pass the file on, gives the person the link's page instead. Both end in ordinary media ids. ```http POST /api/upload-links { "page_id": "", "max_files": 10 } # posts:write GET /api/upload-links/ # posts:write ``` `POST` returns `{ token, url, upload_url, expires_at, max_files }`: `url` is `https://app.dmswift.com/u/`, valid for 30 minutes and up to `max_files` files (default 10, at most 20). The person opens it on a phone or computer and picks files; photos that are not JPEG, or are over 8 MB, are converted to JPEG on the page so they are Instagram-ready. `GET` returns `{ status: "waiting" | "uploaded" | "expired", count, media: [{ media_id, mime, size, name }] }`, in the order the files were picked; use each `media_id` as with `POST /api/uploads`. MCP: `request_upload_link` and `get_uploaded_media`. **Autonomous agents** (an image the agent made itself) skip the page: POST the raw bytes to `upload_url`, one file per request, with the file's own `Content-Type` and optionally `X-File-Name`. No `Authorization` header; the token is the credential. ```bash curl -X POST --data-binary @image.jpg -H "Content-Type: image/jpeg" https://app.dmswift.com/u//file # { "ok": true, "count": 1, "left": 9 } ``` ### Text cards (images dmswift draws) An agent that can reach dmswift only through MCP (a cloud sandbox, a scheduled routine, a chat app without code execution) can send text but not files. For the most common picture on Instagram, words on a background, it does not need to: dmswift renders the JPEG on the server. ```http POST /api/text-cards { "page_id": "", "text": "...", "format": "portrait", "preset": "red-blue", "footer": "@you" } POST /api/text-cards { "page_id": "", "slides": ["1/3 ...", "2/3 ...", "3/3 ..."] } # a carousel GET /api/text-cards/brand?page= # posts:read PUT /api/text-cards/brand?page= { "preset": "midnight", "footer": "@you", "logo": "" } # posts:write PUT /api/text-cards/brand?page= { "reset": true } ``` - **Text:** `text` (one card) or `slides` (1 to 10 cards, carousel order), up to 600 characters each. Line breaks are kept and an empty line is a gap. The text is centered, bold, and sized to the largest that fits; each paragraph is balanced so it does not end on one stray word. Hebrew and Arabic run right to left, mixed with Latin where it appears; emoji are drawn in color. Control characters and bidi override marks are removed. - **Format:** `portrait` 1080x1350 (default), `square` 1080x1080, `story` 1080x1920. A story card is for `ig_type: "story"` only: feed and carousel posts refuse it (`err.ig_aspect_ratio`). - **Style:** `preset` (`red-blue`, `orange-purple`, `aqua`, `purple`, `green`, `fire`, `midnight`, `paper`; paper is light with dark text) or `background: { from, to, angle }` (hex colors; CSS-style angle, default 135 = top-left to bottom-right), `text_color`, `footer` (up to 40 characters, `""` for none), and `logo: false` to leave the kit's logo off. Whatever is not sent comes from the channel's brand kit, then from the `red-blue` preset. - **Brand kit:** one per channel. `PUT` replaces the look (what is not sent is cleared) but keeps the logo unless `logo` is sent: a `media_id` replaces it, `null` removes it. The logo must be a JPEG, PNG or WEBP already in the account, up to 5 MB and 4096 px per side (checked from the file header before anything is decoded); it is scaled to fit 360 px and kept as its own PNG copy, so the original upload being cleaned up later does not break the kit. - **Response:** `{ ok, format, width, height, media_ids: [...], media: [{ media_id, mime, size, sha256, width, height, signed_url }] }`. Each `media_id` works in `create_post` / `update_post` like any upload, on Instagram and on Facebook. Rendering publishes nothing and is not a publish against the daily caps; an unused card is cleaned up after 24 hours like any unattached upload. - **Limits:** 120 cards per account per hour (`429 err.too_many_attempts`); renders run one at a time and a full queue answers `429 err.render_busy` (retry in a few seconds). A text that cannot fit is `400 err.text_card_too_long` and nothing is stored, not even the other slides. - **Safety:** the input is plain text and fixed options. No markup, SVG or URL is accepted, so nothing is fetched and nothing is parsed; the only image ever decoded is the account's own logo, once, after its bytes and header are checked. Each word is measured once and lines are wrapped by arithmetic, so no text can stall the server. ### Campaigns ```http GET /api/rules?page= GET /api/rules/?page= POST /api/rules?page= PUT /api/rules/?page= PATCH /api/rules/?page= { , "version": } DELETE /api/rules/?page= POST /api/test-match?page= { "text": "...", "post_id": "" } ``` `GET /api/rules/` (`campaigns:read`) returns one campaign in the same shape as the list, `hits` and `version` included; a campaign that is not on the named page is a `404` (`err.campaign_not_found_on_page`). `PUT` with a full body replaces the whole rule - read it first and send it back modified, or fields will be cleared. **Partial updates.** `PATCH` (`campaigns:write`) changes only the fields it names, and so does a `PUT` whose body holds nothing but `active` (plus optionally `version` and `page_id`) - that is the on/off switch. A partial update validates only what it **changes**: a problem the stored row already had (for example a message saved before the 640-character limit existed) never blocks switching it off or renaming it, while a problem the update introduces is refused as usual. **Optimistic concurrency.** Every rule carries `version`, bumped by every write. Send the version you loaded as `version` on `PUT`/`PATCH`; if someone saved in between, the write is refused with `409` and `{ "code": "err.rule_conflict", "version": }` - reload and re-apply. A write without `version` keeps last-write-wins. A successful update answers `{ "ok": true, "version": }`. A `version` that is not a positive integer is `400 err.rule_version_invalid`. **Limits** (refused with `400`, never cut silently at send time): | Limit | Code | |---|---| | `name` at most 100 characters | `err.rule_name_too_long` | | `button_url` a complete http(s) address with a domain (`https://shop.com/x`; not `https://` or `https://shop`) | `err.rule_button_url_scheme` (no http/https) or `err.rule_button_url_invalid` | | `button_title` at most 20 characters; `postback_button_title` too, but only where it is sent (`button_url` set, `button_mode: "postback"`, `postback_link_style: "button"`) | `err.rule_button_title_too_long` | | with a button, the message at most 640 characters | `err.rule_dm_640_limit` | | without a button: Facebook 2000, Instagram 1000; on Instagram the link is part of the text, so with a link the cap is `1000 − link length − 1` (and 640) | `err.rule_dm_too_long` | | `button_mode: "postback"` on an Instagram channel (Instagram has no such button) | `err.rule_postback_not_on_instagram` | | `extra_buttons` must be a list of `{ title, url }` | `err.rule_extra_buttons_invalid` | | `extra_buttons` need the first button (`button_url` set) | `err.rule_extra_buttons_need_first` | | at most 3 buttons in all (the first + 2 extras) | `err.rule_buttons_max` | | every extra button needs a `title`, at most 20 characters | `err.rule_extra_button_title_required` / `err.rule_button_title_too_long` | | every extra button's `url` a complete http(s) address with a domain | `err.rule_extra_button_url_invalid` | | `extra_buttons` on a `post_mention` campaign (it has no private message) | `err.rule_dm_not_for_trigger` | With `dm_variants: 1` the limits apply to the longest line, since one line is what a person gets. A refused save answers `{ "error", "code" }` plus, when the problem belongs to one input, `field` - the body field it is about (`name`, `keywords`, `dm_text`, `public_reply`, `post_id`, `button_title`, `button_url`, `postback_button_title`, `postback_text`, `extra_buttons`, `button_mode`). `POST /api/test-match` answers the way the webhook would decide for a top-level comment arriving now: `{ matched, keyword_match, skipped, postMatches }`. `matched` is the automation that would answer right now (schedule applied), or `null`; `keyword_match` is the first automation whose keywords match regardless of schedule; `skipped` lists automations that match but cannot answer now as `[{ id, name, reason: "quiet_hours" }]`. **Matching (`word` mode).** Keywords match whole words, case- and niqqud-insensitive. A Hebrew keyword of three letters or more also matches when the comment glues a prefix onto it - ו, then ש or כש, then one of ב ל כ מ, then ה (so `מחיר` matches `המחיר`, `והמחיר`, `כשהמחיר`). In a phrase only the first word may carry a prefix and the words stay adjacent (`כמה עולה` matches `בכמה עולה`, not `בכמה זה עולה`). A keyword that is only an emoji matches when the comment contains it. `exact` and `contains` modes are unchanged. `test/fixtures/match-vectors.json` is the full contract. **When a matching automation cannot answer.** An automation outside its active hours, or one set to top-level comments only receiving a reply to a comment, steps aside and the next matching automation answers. If none can, the comment is logged with `dm_status` `skipped (quiet hours)` or `skipped (top-level only)` and the first matching automation's `rule_id`. In `postback` mode the button's payload is `SEND_LINK::`, so the tap is logged against the comment it answers; messages sent before this change carry `SEND_LINK:` and keep working. When `postback_text` is empty the follow-up is a default in the account's language (English, or Hebrew when the account's `locale` is `he`). Campaign fields: | Field | Meaning | |---|---| | `name` | required | | `dm_text` | required - the private message | | `trigger` | `comment` (default), `dm`, `story_reply`, `mention` - all four are live | | `keywords` | comma-separated; required unless `match_all` | | `match_mode` | `word` (default, whole-word), `contains` (substring), `exact` (whole trimmed text equals a keyword) | | `match_all` | `1` responds to every comment, ignoring keywords; exclusions still apply | | `exclude_keywords` | comma-separated; any hit prevents a match. Uses whole-word matching in `word` mode and substring matching in `contains` **and `exact`** mode | | `post_id` | empty = every post on the channel. `comment` trigger only. A post-bound campaign is always tried before an all-posts one | | `public_reply` | newline-separated variants, one chosen per comment. `comment` trigger only | | `dm_variants` | `1` treats `dm_text` lines as variants too | | `button_title` / `button_url` | a link on the private message. With a button, `dm_text` is capped at 640 characters | | `button_mode` | `link` (opens the URL) or `postback` (a tap opens the messaging window, then the link is sent) | | `postback_text` / `postback_button_title` / `postback_link_style` | the follow-up after a postback tap; `postback_link_style` is `button` or `exposed` | | `extra_buttons` | buttons 2-3 of the private message: a list of up to 2 `{ "title", "url" }`, always open-link, each link tracked per campaign like the first. Requires `button_url`. **Facebook only** - Instagram sends just the main link. The postback follow-up carries only the main link. `GET` returns the stored JSON string; `PATCH` without it keeps it, a full `PUT` without it clears it | | `like_comment` | `1` auto-likes the triggering comment. **Facebook only** - resolves to a skip on Instagram rather than failing | | `first_level_only` | `1` ignores replies-to-comments | | `public_delay` / `dm_delay` | seconds before each send, `0`-`180` | | `daily_cap` | max DMs per day for this campaign, `0` = unlimited, max `100000` | | `once_per_user` | `1` never messages the same person twice | | `active_start` / `active_end` | quiet hours, minutes past midnight `0`-`1439`, `-1` = off | | `active_days` | CSV of weekday numbers `0`-`6`, empty = every day | | `active` | `0` pauses the campaign | ### Analytics ```http GET /api/stats?page= GET /api/stats/hourly?page=&tz= GET /api/stats/daily?page=&tz=&days=<7|30> GET /api/stats/top-rules?page= GET /api/log?page=&limit=&group=<0|1>&before=&commenter=&since=&until=&q= ``` `GET /api/stats` returns `total`, `dm_ok`, `last24h`, `unique_people`, `clicks`, `dm_week`, `dm_prev_week`, `recent_errors` and `last_error`. Button-tap follow-ups are reported as `clicks` and excluded from the DM counters, so one recipient is never counted twice. Group comments are excluded from every counter - see below. `GET /api/log` returns at most 500 rows (default 100). `group=1` switches it to the opt-in view of group comments: comments that arrived from a Facebook Group, which reach the webhook but which Meta allows neither a reply nor a private message on. They are recorded but excluded from every count, chart and default listing. The other filters are optional and combine with each other and with `group`: | Param | Meaning | |---|---| | `before` | rows with `id` below this - pass the last `id` you received to page backwards | | `commenter` | one person's rows (`commenter_id`, exact) | | `since` / `until` | inclusive UTC time range: ISO 8601 (`2026-10-01T08:00:00Z`) or `YYYY-MM-DD[ HH:MM:SS]`; a bare date as `until` means the end of that day | | `q` | substring search over the commenter's name, the comment text and the campaign name; `%` and `_` match themselves | A malformed value is refused with `400` and `code: "err.log_bad_filter"` rather than ignored. Rows written by the retry worker (`comment_text: "(retry attempt)"`) carry the original `commenter_id`, and a button tap from a new-format DM carries the `comment_id` it answers - and the commenter's id only when the person who tapped is the one the private message was delivered to (a forwarded message tapped by someone else stays under the tapper's own id). Every row also carries two derived booleans, so a client never re-implements either rule: - `is_group` - the server's group-comment test. A bare comment id (no `_`) marks a Facebook group comment **only on a Facebook channel**; Instagram comment ids are always bare and are never group rows. Rows with a `skipped (group comment…)` status are group rows on any channel. - `delivered_later` - this row's private message failed, and a later retry of the same comment on the same channel delivered it. Such a row is not a "not delivered" failure. Failure counts everywhere (`/api/stats` `recent_errors` and `last_error`, analytics `errors`, `dms_failed`, the error breakdown) leave out failures that were delivered later. The richer reports live under `/api/analytics/*` and are open to `analytics:read`: ```http GET /api/analytics/overview?page=&days=<7|14|30|90> GET /api/analytics/funnel?page=&days=&rule= GET /api/analytics/campaigns?page=&days= GET /api/analytics/timeseries?page=&days=&metric= GET /api/analytics/hourly?page=&days= GET /api/analytics/weekday?page=&days= GET /api/analytics/keywords?page=&days=&limit= GET /api/analytics/unanswered?page=&days=&limit= GET /api/analytics/people?page=&days= GET /api/analytics/errors?page=&days= GET /api/analytics/best-times?page=&days=&limit= ``` Two measures appear in `overview` (`metrics` and `totals`) and in `timeseries`: - `answered` - incoming comments where the public reply **or** the private message succeeded, including a private message the retry worker delivered later. Group comments never count. - `link_opens` - opens of this channel's tracked links in the window, crawlers excluded. Not the same as `clicks`, which is the Messenger button tap that asks for the link. The link sent AFTER a tap is tracked too (so the person's opens are known), but its opens are left out of `link_opens`: the tap already is that person's click. `campaigns` adds `link_clicks` per campaign: tracked-link opens of that campaign's links in the window (follow-up links after a tap excluded, as above). `unique_people` counts people the automation actually reached - a delivered private message or a public reply that landed - not people who were skipped or whose message failed. New series and CSV columns are appended after the existing ones. **Tracked links and `?r=`.** A link in a private message carries `r=.`. Only a verified signature files an open under a person; anything else still redirects and counts as an anonymous open. Links already in people's inboxes carry an unsigned `r=`, which is believed only when that recipient really received a message from the link's automation on that channel. `days` is an allowlist - **7, 14, 30 or 90**. Anything else silently becomes 30. There are no arbitrary from/to dates and no sub-day granularity. The timezone comes from the account, never from the query string: a caller who can move the day boundary can manufacture any number they like. Rate limit: 240 report requests per account per minute, answered with `429` and `code: "err.analytics_rate_limited"` plus a `Retry-After` header. ```http GET /api/analytics/export.csv?page=&report=&days= ``` The CSV export **is** open to `analytics:read`. It is listed in the key route table as its own literal entry rather than by widening the `/analytics/` pattern to allow a dot - so adding a new report can never accidentally expose a bulk download along with it. 20 exports per 5 minutes. ### Contacts ```http GET /api/contacts?page=&search=&tags=&platform=&subscribed=&source=&seen_within_days=&messaged=&limit=&offset= GET /api/contacts/stats?page= GET /api/contacts/tags?page= GET /api/contacts/?page= GET /api/contacts//clicks?page= POST /api/contacts//tags?page= { "page_id": "", "tags": ["lead"] } GET /api/contacts/export.csv?page=& ``` `messaged=1` keeps only people this channel has sent at least one message to (`messaged=0` the rest); a message the retry worker delivered later counts. Every contact row carries `clicked` (boolean) and `last_click_at` (or `null`): the person opened a tracked link, or tapped the button that got them the link. `GET /api/contacts//clicks` (`contacts:read`) returns `{ clicks: [{ id, ts, link_id, code, title, target_url, rule_id }], taps: [{ id, ts, rule_id, rule_name, comment_id }] }` - that person's tracked-link opens and button taps on this channel, newest first, at most 200 each, crawlers excluded. `GET /api/contacts/export.csv` returns the **People CSV** by default: one row per person with headers in the account's language (English or Hebrew) - name, username, platform, automation, comment, private message (yes/no), link click (yes/no), first contact, last contact, tags, ID, then any custom fields. `format=raw` returns the original machine export (`external_id,platform,name, username,tags,source,subscribed,first_seen,last_seen` + custom fields). Any cell that starts with `=`, `+`, `-` or `@` - also after leading whitespace - is prefixed with `'` so spreadsheets do not run it. Tag filtering is **AND** - every named tag must be present. Up to 20 tags per query, 200 rows per request. A contact can hold 50 tags, each up to 40 characters; tags are lowercased and stripped of commas on the way in. `source` accepts `comment`, `dm`, `story_reply`, `mention`, `referral`, `manual` or `import`. `referral` is somebody who arrived through a growth link - a QR code, a bio link, an ad - which is the acquisition story a customer most wants to segment on. `GET /api/contacts/export.csv` **is** open to `contacts:read`, capped at 20,000 rows and 10 exports per user per minute. Editing a contact, deleting one, opting someone in or out and managing segments remain **session-only**: they change a customer's own records, which stays with a human. ### Knowledge base ```http GET /api/kb?page=&q=&kind=&limit=&offset= GET /api/kb/summary?page= POST /api/kb/search?page= { "page_id": "", "query": "what are your hours" } POST /api/kb?page= { "page_id": "", "title": "...", "body": "...", "kind": "faq" } ``` `kind` is one of `faq`, `note`, `policy`, `product`; anything else becomes `faq`. Title 200 characters, body **8,000 characters** - an over-long body is refused, not truncated, so split it. 500 entries per channel. Search returns 4 passages by default and 20 at most, drops anything scoring below 20% of the top hit, and is limited to 60 searches per user per minute. Retrieval is lexical (SQLite FTS5 with BM25, title weighted 10× body), **not embeddings**. A question phrased with none of the words in the entry will not find it. Editing, deleting and bulk import are session-only. ### Comments, insights and the inbox All take the channel as `page_id` (query on reads, body on writes; `?page=` works too); a pinned key must name it. | Method and path | Scope | Answer | |---|---|---| | `GET /api/comments?page_id=&post_id=&limit=25&cursor=&since=` | `comments:read` | `{ comments: [{ id, post_id, platform, from: { id, name }, text, created_time, like_count, reply_count, hidden, parent_id }], next_cursor }` | | `POST /api/comments/:id/reply` `{ page_id, text }` | `comments:write` | `{ ok: true, id }` - counts as a comment; 8,000 characters on Facebook, 2,200 on Instagram; `403 approval_required_for_comment` for approval-mode keys | | `POST /api/comments/:id/hide` `{ page_id, hidden }` | `comments:moderate` | `{ ok: true }` | | `DELETE /api/comments/:id?page_id=` | `comments:moderate` | `{ ok: true }` | | `POST /api/comments/:id/like` `{ page_id }` | `comments:write` | `{ ok: true }`; Instagram → `400 unsupported_on_instagram` | | `GET /api/insights?page_id=&period=day\|week\|days_28&since=&until=` | `insights:read` | `{ platform, period, metrics: { followers, new_followers, reach, impressions_or_views, engagements, … }, series: { metric: [{ date, value }] } }`. `followers` = current total; `new_followers` = follows gained in the window (Facebook `page_daily_follows_unique`, plus `unfollows`; Instagram `follower_count`, reported by Meta only for 100+ followers). If insights are refused but the follower count answers, the response carries `permission_missing` instead of failing | | `GET /api/insights/posts/:postId?page_id=` | `insights:read` | `{ platform, post_id, metrics: { reach, views, likes, comments, shares, saves? } }` | | `GET /api/inbox?page=&status=open\|done\|snoozed\|all&q=&unread=1&limit=30&offset=` | `inbox:read` | `{ rows, total, counts: { open, unread, done, snoozed } }`, newest first; each row carries the person's `name` / `username`, `last_preview`, `unread`, `window_open`, `takeover_active` | | `GET /api/inbox/unread-count?page=` | `inbox:read` | `{ messages, conversations }` | | `GET /api/inbox/:id?page=&before=&limit=` | `inbox:read` | `{ conversation, contact, messages, has_more, window_open, window_expires_at }` - messages oldest first; `before` is a message id | | `POST /api/inbox/:id/reply?page=` `{ page_id, text }` | `inbox:write` | sends; checked in this order: `403 approval_required_for_dm` for approval-mode keys, `409 human_takeover` while a human took over, the daily `dm` cap (100/day), then `409 err.inbox_window_closed` outside Meta's 24-hour window and `409 err.inbox_opted_out`. Recorded as `sent_by: "ai"`; never takes the thread over | Two behaviours worth knowing: without `post_id`, `GET /api/comments` reads the 10 most recent posts and its `next_cursor` pages through **posts**, not comments; with `post_id`, Facebook returns comments and replies as one flat list, newest first, and Instagram returns each top-level comment followed by its replies. Graph refusals come back as `permission_missing` (403, with the `permission` to reconnect for), `err.token_invalid` (409), `comment_not_found` (404), `graph_rate_limited` (429), `graph_timeout` (504) or `graph_error` (400). ### Account (dashboard session only - closed to API keys) ```http GET /api/me → { id, email, name, role, tz_offset, tz, bot_paused, locale, ... } GET /api/billing → { plan, usage, status, expired, at_limit, trial_end, plans, upgrade_request } POST /api/billing/upgrade { plan } → records an upgrade request POST /api/toggle-pause { paused: true|false } → { ok, paused } GET /api/onboarding → { steps[], complete } GET /api/meta-app → the tenant's own Meta app + the URLs to paste at Meta POST /api/meta-app { app_id, app_secret } ``` `GET /api/me` is the one exception: it is in the key route table with no scope requirement, so any key may ask who it is. The rest of this group is session-only. `tz` is the account's IANA time zone (`Asia/Jerusalem`), or `''` when none is set; when it is set, `tz_offset` is that zone's offset in minutes right now, so daily caps, active hours and reports follow daylight saving. Only zone names are accepted (raw offsets such as `+23:59` are `400 err.tz_invalid`), and every offset is clamped to −720…840. `upgrade_request` is the pending `{ plan, at }` request, or `null`. `POST /api/billing/upgrade` accepts only a plan listed in `plans` (otherwise `400 err.plan_not_found`). `trial_end` is `null` unless the account is in a trial that has not ended. Then it is `{ ends_at: "YYYY-MM-DD", days_left, falls_back_to: { code, name, max_pages, max_campaigns, max_dms }, over_limits: [{ resource: "pages"|"campaigns"|"dms", used, limit }] }` - the plan the account moves to the day after `ends_at`, and every current usage already over that plan's limits, so the dashboard can warn before it happens. There is no free plan: `falls_back_to` is the `frozen` plan ("Paused"), whose limits are all `0` - automations stop until a plan is chosen, while sign-in, export and read-only API/MCP access keep working. `POST /api/toggle-pause` requires an explicit boolean: `{ "paused": true }` pauses every automation on the account, `{ "paused": false }` resumes. Anything else is `400 err.paused_boolean_required` and changes nothing. Purging a channel (or a commenter's data-deletion request) deletes its activity rows, but this month's delivered messages stay counted in `usage.dms`: they are moved to the billing ledger first. `plan.max_*` limits use `-1` for unlimited. The plan carries `max_pages`, `max_campaigns`, `max_dms`, `max_posts`, `max_ai_replies`, `max_broadcasts` and `max_seats`. Usage counters are per calendar month for `dms`, `posts`, `ai_replies` and `broadcasts`, and current totals for `pages` and `campaigns`. Usage is derived by counting real rows rather than incremented into a counter, so it stays correct across deletes, retries and backfills. What each limit counts, exactly as the gates enforce it: - `max_dms` - messages the platform sends automatically: campaign replies (comment or message trigger), broadcast and sequence sends, growth-link greetings and Instagram mention replies. Replies a person types in the inbox and button-tap follow-ups are not counted. - `max_campaigns` - every saved campaign on every channel of the account, active or paused (a current total, not a monthly count). Deleting one frees its slot. - `max_posts` - posts the platform publishes on Facebook or Instagram in the month (a reel, story or carousel is one post). Drafts, uploads and local schedules are not counted until they publish; a post handed to Facebook's native scheduler holds its slot from the moment it is scheduled. A scheduled post over the limit is deferred, not failed. - `max_seats` - people invited besides the owner (members plus pending invites); the owner never takes a seat. Monthly meters start on the 1st (UTC); during a trial they start on the trial's first day, so a trial that crosses a month boundary gets its allowance once. Two things about `usage` are worth reading closely, because they changed: - **`dms` is the whole monthly message allowance, whatever sent the message.** Campaign replies, inbox automation, broadcast sends and sequence steps all count against the one number. The parts are still reported beside it as `broadcast_sends` and `sequence_sends` for a usage screen, but the gate is the total. Two ledgers, one of which the comment path could not see, is how a tenant used to exhaust their allowance through broadcasts and keep sending campaign replies. - **`broadcasts` counts runs, not messages** - and a run that is merely *scheduled* is already counted. Counting only runs that had started meant six could be queued against a limit of two, because none of them had begun yet. Button-tap follow-ups and a human's manual inbox reply are excluded from `dms`. Every limit above is enforced today, including `max_ai_replies` (checked before the model is called, so no token is spent to discover a refusal) and `max_broadcasts`. --- ## 5. Outbound webhooks The generic way this product plugs into everything else: Zapier, Make, n8n, a CRM or your own backend. You register an HTTPS endpoint per channel, subscribe it to events, and dmswift POSTs a signed JSON envelope whenever one fires. Endpoints are managed from the dashboard (**session-only**, see §7): `GET|POST /api/webhooks`, `PUT|DELETE /api/webhooks/:id`, `POST /api/webhooks/:id/test`, `GET /api/webhooks/:id/deliveries`, `POST /api/webhooks/:id/deliveries/:did/retry`. Up to **10 endpoints per channel**. The signing secret is returned **once**, in the response to `POST /api/webhooks`. It is sealed at rest afterwards and is never returned again, not even partially - recovery means rotation. ### Event catalog | Event | Fires when | Emitted from | |---|---|---| | `comment.received` | a comment arrived on a connected channel, before any rule ran | the comment webhook | | `comment.matched` | a campaign matched that comment | the comment webhook | | `dm.sent` | a private message went out - a campaign reply, an AI answer, or a growth-link greeting | the comment webhook, the AI path, the referral handler | | `dm.failed` | the send was refused, permanently or after retries | the same three | | `link.clicked` | a tracked link was opened by a human (bot traffic never emits) | the `/l/:code` redirect | | `contact.created` | first time this person has been seen on this channel | the contact upsert | | `contact.tagged` | a tag was added to a contact | the tagging path | | `contact.optout` | the person asked to stop hearing from this channel | the inbox opt-out path | | `message.received` | an inbound DM, story reply or mention from a person | the messaging webhook | | `conversation.opened` | a new conversation thread started | the inbox | | `broadcast.finished` | a broadcast run completed | the broadcast worker | | `post.published` | a scheduled post went live | the post worker | | `campaign.created` | a campaign was created, by a human or an API key | the campaign route | | `quota.exhausted` | the account hit its monthly message limit and a reply was skipped | the comment webhook | All fourteen are subscribable and **all fourteen now have an emitter**. This table used to carry a "not yet" column for nine of them; that column is gone because it is no longer true. Two things it is still worth knowing rather than discovering: - `quota.exhausted` currently fires for one resource - the monthly message allowance (`resource: "dms"`), from the comment path. Hitting the post, AI or broadcast limit is refused with a `402` but does not emit. - `dm.sent` and `dm.failed` carry a `source` field (`growth`, `ai`) when the message did not come from the ordinary comment path. Branch on it if the distinction matters to you; its absence means a campaign reply to a comment. `ping` is not in the catalog and cannot be subscribed to. It exists only for the "test this endpoint" button, which sends it inline and reports the real response. ### Payload envelope Every delivery is a `POST` with `Content-Type: application/json` and this body: ```json { "event": "dm.sent", "page_id": "1247943345074815", "sent_at": "2026-08-11T09:14:22.481Z", "data": { "comment_id": "1234567890_0987654321", "rule_id": 12, "rule_name": "Price inquiries", "contact_id": 4471, "to": "6301234567890123" } } ``` `data` is event-specific. If the whole envelope would exceed **64 KB** it is replaced with `data: { "truncated": true, "bytes": }` - the event still arrives, the payload does not. Headers on every delivery: | Header | Value | |---|---| | `User-Agent` | `dmswift-webhooks/1` | | `X-Dmswift-Event` | the event name | | `X-Dmswift-Delivery` | delivery id - stable across retries of the same delivery, use it to dedupe | | `X-Dmswift-Timestamp` | unix **seconds** at send time | | `X-Dmswift-Signature` | `sha256=` - see below | ### Signature ``` signed material = "" + "." + "" X-Dmswift-Signature = "sha256=" + hex(HMAC_SHA256(endpoint secret, signed material)) ``` The timestamp is inside the signed material, not merely alongside it: signing the body alone would make every captured request replayable forever, since the signature would stay valid. So a receiver checks **two** things - that the HMAC matches, and that the timestamp is recent - and compares the digests in constant time. A 5-minute tolerance is what dmswift's own verifier uses. The body must be hashed as the **raw bytes that arrived**. Re-serializing parsed JSON will change key order or whitespace and the signature will not match. **`X-Dmswift-Delivery` is not covered by the signature.** The signed material is the timestamp and the body, and the delivery id travels in an unsigned header. It is still the right key for deduplication - it is stable across retries of the same delivery, which nothing in the body is - but treat it as a hint from an authenticated sender rather than as signed data, and make your handler idempotent on the event's own contents where you can. ### Verifying in Node ```js import express from 'express'; import { createHmac, timingSafeEqual } from 'node:crypto'; const app = express(); const SECRET = process.env.DMSWIFT_WEBHOOK_SECRET; const TOLERANCE_S = 300; // express.raw, not express.json: the signature covers the bytes on the wire. app.post('/hooks/dmswift', express.raw({ type: 'application/json' }), (req, res) => { const ts = req.get('X-Dmswift-Timestamp'); const sig = req.get('X-Dmswift-Signature') || ''; const raw = req.body.toString('utf8'); // 1. the timestamp must be recent - this is what stops replay const age = Math.abs(Math.floor(Date.now() / 1000) - Number(ts)); if (!Number.isFinite(age) || age > TOLERANCE_S) return res.sendStatus(400); // 2. the HMAC must match, compared in constant time const expected = 'sha256=' + createHmac('sha256', SECRET).update(`${ts}.${raw}`, 'utf8').digest('hex'); const a = Buffer.from(expected, 'utf8'); const b = Buffer.from(sig, 'utf8'); if (a.length !== b.length || !timingSafeEqual(a, b)) return res.sendStatus(401); const { event, page_id, data } = JSON.parse(raw); // 3. answer fast. Anything slow belongs on your own queue - // dmswift gives up on the connection after 10 seconds. res.sendStatus(200); handleAsync(event, page_id, data, req.get('X-Dmswift-Delivery')); }); ``` ### Delivery, retries and failure - **Success is any 2xx.** Anything else is a failure, including 3xx: redirects are never followed. - **Timeout: 10 seconds.** Answer immediately and do your work afterwards. - **6 attempts**, backing off roughly `0s, 30s, 2m, 10m, 1h, 6h` - about eight hours end to end. Long enough to ride out a deploy or a certificate renewal, short enough that a delivery is either done or reported the same day. - **`410 Gone` is honoured immediately.** It is the one status that is a statement about the endpoint rather than about the request: the delivery fails at once and the endpoint is disabled with `disabled_reason: "gone"`. - **20 consecutive failures disables the endpoint** (`disabled_reason: "failures"`). Any 2xx resets the run to zero. - Deliveries are queued rows worked by a bounded tick - at most 25 per tick and 5 per endpoint - so one dead receiver cannot starve everyone else's. - Delivery records are kept **30 days** when delivered and **90 days** when not. Failures are kept longer because they are the ones somebody investigates. Retries are at-least-once. Use `X-Dmswift-Delivery` to make your handler idempotent. ### Destination restrictions The destination is a URL you type, and this process sits in a private network, so: - `http://` and `https://` only; - no `user:password@` in the URL - use a header or a query parameter; - URLs up to 500 characters; - addresses that resolve inside a private network are refused, at save time **and again at connect time**, through a guarded DNS lookup - the address that is checked is the address that is dialled, so there is no rebinding window; - `localhost`, `.local`, `.internal`, `.home.arpa` and bare single-label hostnames are refused by name as well as by address; - redirects are never followed. --- ## 6. Errors Errors are JSON with an `error` message, and a machine-readable `code` where the reason is structural: | HTTP | `code` | Meaning | What to do | |---|---|---|---| | 401 | - | Key or token missing, unknown, revoked or expired. `/mcp` adds `WWW-Authenticate: Bearer resource_metadata="…"` (and `error="invalid_token"` for a bad one) | An OAuth client refreshes or signs in again; a key holder stops. | | 403 | `route_forbidden` | Endpoint is closed to API keys entirely | Stop. This is not scope-fixable. See §7. | | 403 | `missing_scope` | Key lacks the scope in `required` | Stop; a broader key is needed. | | 403 | `page_forbidden` | Key is pinned to other Pages | Use a Page from `list_pages`. | | 400 | `page_required` | Key is pinned to specific Pages and the request named none | Add `?page=`. | | 402 | `plan_limit` | The account's plan does not allow this (quota used up, or a feature not included). Body: `{ error, code: "plan_limit", kind?, message_key, upgrade_url }` - `kind: "agent_actions"` when the monthly allowance of key writes is spent | Stop. Only an upgrade or the next month clears it; pass `upgrade_url` to the owner. | | 429 | `daily_cap` | A daily cap of this key is spent. Body: `{ error, code: "daily_cap", kind: "publish"\|"comment"\|"dm", cap, used, remaining, resets_at }` | Stop that kind of action until `resets_at` (UTC midnight). | | 429 | `err.rate_limited`, `err.analytics_rate_limited`, `err.too_many_attempts` | A per-action rate limit; `retry_after` / `Retry-After` says how long | Wait, then retry. **These are the only retryable 4xx.** | | 409 | `err.rule_conflict` | The rule changed since the `version` you sent; the body carries the current `version` | Re-read the rule, re-apply your change, send again. | | 415 | `err.unsupported_content_type` | A write under `/api` carried a body that is not `application/json` | Send JSON. | | 403 | `err.cross_site_request` | A cookie-authenticated write that a browser marked `Sec-Fetch-Site: same-site` or `cross-site` | Not reachable from an API key (bearer requests are exempt). | | 403 | `approval_required_for_campaign` | An approval-mode key ("ask me first") tried to create or switch on an active campaign | Save it with `active: false` and ask the owner to turn it on. | | 403 | `approval_required_for_comment` | An approval-mode key tried to reply to a comment | Show the owner the text; they send it from the dashboard. | | 403 | `approval_required_for_dm` | An approval-mode key tried to send a private reply | Same - hand the text to the owner. | | 409 | `human_takeover` | A person on the team took the conversation over | Do not reply; leave it to them. Not retryable until they release it. | | 404 | `post_not_found` | The `post_id` is not a post of this channel | Use a post from `list_posts` / `GET /api/posts` for the same `page`. | | 404 | `comment_not_found` | Meta says the comment no longer exists (deleted or hidden by its author) | Skip it; refresh the thread. | | 400 | `err.media_fetch_rate_limited` | More than 60 downloads by URL (`media_urls`, `/api/uploads` by URL, a comment's `media_url`) in an hour for this account | Wait an hour, or send the file as base64 (`media_data`) instead. | | 400 | `err.ig_aspect_ratio` | An Instagram feed or carousel photo is outside 4:5 to 1.91:1 (judged on JPEGs without EXIF rotation) | Post it as a story, or render/crop it to portrait or square. | | 400 | `err.text_card_too_long` | A text card's text, or a footer, does not fit or is over its limit | Shorten it or split it into slides. | | 400 | `err.text_card_text_required` / `err.text_card_too_many_slides` / `err.text_card_invalid_option` / `err.text_card_invalid_color` / `err.text_card_logo_invalid` | A text card request is malformed | Fix the field the message names. | | 429 | `err.render_busy` | The text-card renderer's queue is full | Retry once in a few seconds. | | 503 | `err.render_unavailable` | The text-card renderer is down on the server | Do not retry now; upload an image instead. | | 400 | `err.checksum_mismatch` | A base64 upload's bytes do not hash to `expected_sha256`; nothing was stored | Encode and send it again; after two failures use `request_upload_link`. | | 400 | `err.invalid_checksum` | `expected_sha256` is not 64 hex characters | Send the lowercase or uppercase hex digest. | | 400 | `err.invalid_image` | A JPEG or PNG upload is cut off or corrupted; nothing was stored | Send the complete file again. | | 400 | - | Validation failure; `error` explains in English | Fix the input and retry. | Every `/api` response carries `Cache-Control: no-store`. Apart from the 429s, none of the 4xx conditions above are transient. Retrying an identical request will fail identically. Some `code` values are dotted i18n keys (`err.page_not_found`, `err.no_page_connected`, `err.rule_keywords_required`, …). Those are stable and safe to branch on, but the bare codes in the table above are the ones the API contract guarantees. `plan_limit` now also covers **team seats**: an invite that would exceed the account's `max_seats` is a `402` with `code: "plan_limit"` and `message_key: "err.team_seat_limit"`, exactly like every other quota refusal. That path is session-only (§7), so an API key will never see it - but the code is shared, so a client that already branches on `plan_limit` needs no change. The features added since this document was first written are all session-only, and their error codes are therefore never returned to a key. They are listed here so a developer reading a dashboard network log can look one up rather than guess: | Surface | Codes | |---|---| | A/B tests | `err.abtest_not_found`, `err.abtest_exists`, `err.abtest_rule_not_found`, `err.abtest_too_few_variants`, `err.abtest_rotation_off`, `err.abtest_no_tracked_link`, `err.abtest_not_decided`, `err.abtest_winner_missing`, `err.abtest_bad_metric`, `err.abtest_bad_status`, `err.abtest_no_page`, `err.abtest_rate_limited` | | Growth links & QR | `err.growth_slug_required`, `err.growth_slug_invalid`, `err.growth_slug_too_long`, `err.growth_slug_taken`, `err.growth_not_found`, `err.growth_rule_not_found`, `err.growth_no_page`, `err.growth_no_username`, `err.growth_limit_reached`, `err.growth_qr_empty`, `err.growth_qr_too_long`, `err.growth_qr_ecc`, `err.growth_qr_busy` | | Team | `err.team_forbidden`, `err.team_rank_forbidden`, `err.team_role_invalid`, `err.team_role_too_high`, `err.team_seat_limit`, `err.team_email_invalid`, `err.team_already_member`, `err.team_owner_self`, `err.team_owner_inactive`, `err.team_invite_pending`, `err.team_invite_invalid`, `err.team_invite_used`, `err.team_invite_expired`, `err.team_invite_email_mismatch`, `err.team_invite_not_found`, `err.team_member_not_found`, `err.team_rate_limited`, `err.team_failed` | | Alerts | `err.alerts_bad_config`, `err.alerts_unknown_kind`, `err.alerts_test_throttled` | Status codes for that group: the `*_rate_limited` / `*_busy` / `*_throttled` codes are `429` and carry `Retry-After`; `err.team_seat_limit` is a `402`; `err.team_forbidden` and `err.team_rank_forbidden` are `403`; the team `_not_found` codes are `404`. On the A/B and growth routes a `404` means the route resolved the id and found nothing on this channel, while the same `_not_found` code raised from deeper in a write comes back as a `400` - so branch on the `code`, never on the status alone. Everything else in the group is a `400`. ### 202 - pending approval A key may be configured to require human approval (for an OAuth connection: "Ask me before anything is published" on the consent screen). Such a key cannot schedule (`err.approval_required_no_schedule`), comment in public (`err.approval_required_no_comment`, or `approval_required_for_comment` on a comment reply) or send private messages (`approval_required_for_dm`). Its `publish_post` calls do not publish; they return **HTTP 202**: ```json { "ok": true, "status": "pending_approval", "code": "pending_approval", "approval_id": 7, "message": "Submitted for human approval..." } ``` **This is a success, not a failure.** The request is queued and the owner has been notified; the post publishes when they approve. Do not retry - a retry returns `duplicate: true` and queues nothing. Do not treat the post as published: check `get_post` later, and consider the job done once its status is `published`. --- ## 7. What API keys cannot reach, and why Machine access is **fail-closed**. `src/server.js` holds an explicit table (`KEY_ROUTES`) of every method-and-path an API key may use; anything absent from it answers `403 route_forbidden` no matter what scopes the key carries. This means adding a route never silently widens machine access - but it also means a route is unreachable until somebody adds it to the table. Three groups are closed. **Closed on purpose - administration.** No scope will ever open these: - API key management (`/api/keys`, and the connected OAuth apps at `/api/keys/oauth-apps`) - a key must never mint another key or a stronger sibling. The one exception is `GET /api/keys/self`, which only describes the calling key. - Account administration and user management (`/api/admin/*`). - Meta app credentials (`/api/meta-app`), notification, LLM and content-agent settings. - Billing and upgrades (`/api/billing*`), onboarding, page connect/disconnect. - Approvals (`/api/approvals*`) - the whole point is that a human decides. - Team management (`/api/team*`) - a machine credential that can invite people can quietly grant a human permanent access to the account. Those routes refuse an API key with a second, explicit check inside the module, so they stay closed even if somebody adds them to the route table by accident. **Closed on purpose - destructive and outbound.** A key may read and add, but not destroy or broadcast: | Surface | Why | |---|---| | Editing/deleting contacts, opt-in/opt-out, segments | changing a customer's own records is a human decision. A key may tag, and that is all | | Editing/deleting knowledge base entries, bulk import | a key may add a fact; unmaking one stays with a human | | Broadcasts and sequences (`/api/broadcasts*`, `/api/sequences*`) | sending to a whole segment is the single action in the product that reaches the most people at once. It is not something an autonomous agent should be able to trigger | | Inbox management (`/api/inbox/:id/status`, `takeover`, `note`, `tags`, canned replies) | the inbox exists for a human to take over. Reading it (`inbox:read`) and replying inside Meta's 24-hour window (`inbox:write`, never while a human took over, never for approval-mode keys) are open since contract C3 | | Moderation rules (`/api/moderation*`) | automatic hide/delete rules act on other people's comments without anyone looking. One-off hide and delete are open as `comments:moderate` | | Outbound webhook config (`/api/webhooks*`) | a key could otherwise redirect the account's event stream to itself. Deliveries themselves are unaffected | **Closed by omission.** These are ordinary tenant features whose routes were simply never added to `KEY_ROUTES`. They work in the dashboard and return `403 route_forbidden` to a key: | Surface | Routes | |---|---| | Tracked links | `/api/links*` - the public redirect `/l/:code` needs no auth and is unaffected | | Audit log | `/api/audit*` | | AI configuration and drafting | `/api/ai/*` | | A/B tests | `/api/abtests*` | | Growth links and QR codes | `/api/growth*`, including `/api/growth/qr.svg` | | Alerts | `/api/alerts*` | The two CSV exports that used to sit in this table - `/api/analytics/export.csv` and `/api/contacts/export.csv` - **have since been added to the route table** and are now reachable with `analytics:read` and `contacts:read` respectively. They are listed as their own literal entries, not by widening the pattern that matches the JSON reports. If you need something from the table above, the fix is a server change, not a different key. ### Why the newest features are session-only They were not overlooked. Each one is closed for a reason a scope could not fix: - **A/B tests** (`/api/abtests*`) - the write side rewrites a campaign's `dm_text` down to a single line and throws the other variants away (`POST /api/abtests/:ruleId/apply`, or `auto_apply` on the test). That is a destructive edit of a customer's message copy, made on the strength of a statistic that is explicitly a decision aid rather than a proof. The **read** side is genuinely useful to an agent and could be opened later; it has not been, because opening half a module is how the other half gets opened by the next person who reads the table. - **Growth links** (`/api/growth*`) - a growth link is a public identifier that gets printed. An agent that can mint, rename or delete slugs can invalidate a poster that is already on a wall, and `qr.svg` is an unbounded CPU cost (eight masked matrices, scored) behind a cheap request. - **Team** (`/api/team*`) - see above: it hands out standing access to a business. - **Alerts** (`/api/alerts*`) - `POST /api/alerts/test` sends real mail to the account's address. A key that can push that button is a spam cannon aimed at the owner. None of these appear in `KEY_ROUTES`, so all of them answer `403 route_forbidden` regardless of scope. There are no MCP tools for them either - MCP is a thin adapter over the same REST routes, so a tool would fail exactly the same way. --- ## 8. Rules for agents 1. **Call `list_pages` first.** Every other operation needs a channel id, and guessing one fails. Check `platform` on the result - Instagram posts need media and an `ig_type`, and a campaign that likes comments will not like them there. Unsure what you may do today? `get_account_status`. 2. **Publishing is public and irreversible.** Never publish content a human has not seen. Draft, show it, then publish. 3. **Use `dry_run`** on `publish_post` when you are unsure - it validates without publishing. 4. **Verify campaigns with `test_campaign_match`** before the post is live. A campaign with a too-broad keyword will message everyone who comments. 5. **Do not retry 4xx**, except the 429s in the error table, which carry a `retry_after`. 6. **Respect the daily caps** (publishes, comments, messages). They exist to bound the damage of a loop, not to be worked around: on `daily_cap`, stop that kind of action until `resets_at`. Follow `next_step` on any MCP error. 7. **Check `get_activity_log`** after a campaign goes live to confirm messages are actually being delivered rather than silently failing. 8. **A 202 means a human is deciding.** Stop and report that; do not loop waiting for it. 9. **`route_forbidden` is final.** It is not a scope problem and not a transient one - the route is not open to keys at all. Report it and stop rather than trying variations.