# Selda documentation Generated from https://docs.selda.ai. Every page, in reading order. # Welcome to Selda **Find your first customers with Selda.** You describe what you built or who you want to reach. Selda researches each prospect, writes the message, and sends it from your own inbox. You approve before anything goes out. The difference is what Selda knows before it writes. For a prospect who just opened a second location, most AI outreach sends: > "Hi, saw your company is growing fast, impressive! I help businesses like yours. Open to a quick chat?" Selda sends: > "You just opened your second clinic and you're hiring two more hygienists. Most booking setups break right at that two-location handoff. Worth 15 minutes?" Same prospect. One read the website. That gap is the whole product. ## Who it's for Startups and builders, professional sellers, whole teams. Anyone who needs customers. ## Five ways in, one machine behind them Every door below reaches the same Selda. Pick whichever fits how you work. Nothing sends from any of them until you approve it in the app. | | | | --- | --- | | **[The app](/ways-to-use/web-app)** | The main one. Build a campaign step by step, approve the messages, handle the replies. Start here. | | **[Telegram](/ways-to-use/telegram)** | Selda in a chat on your phone. Say what customers you need, send a photo of a list, ask where things stand. | | **[Your website](/ways-to-use/website-widget)** | One script tag on your own site, on WordPress or anything else. Visitors' enquiries become leads with a reply drafted. | | **[Run it from your AI](/ways-to-use/mcp-server)** | Paste one address into ChatGPT, Claude, Cursor or Gemini and it runs your workspace. It cannot send. | | **[The API](/connect-your-app)** | For connecting your own software. One endpoint pattern, one auth header, no SDK. | Not sure what to say to it? [Prompts that do something](/ways-to-use/prompts) is a page of them, ready to copy. ## Code and plugins Three repositories, one job each. Everything else is on this site. | | | | --- | --- | | **[selda-wordpress](https://github.com/Selda-AI/selda-wordpress)** | The WordPress plugin. Captures the forms you already have and sends submissions to Selda. | | **[Selda MCP](https://github.com/Selda-AI/Selda-mcp-docs)** | Connecting an AI client: tools, schemas and loadable skills. | | **selda-api** | Runnable examples for the HTTP API in curl, Node, Python and PHP. | ## Next steps - [Getting started](/getting-started): create a workspace and run your first campaign. - [How Selda contacts people](/how-selda-contacts-people): what actually goes out, and from where. - [Reference](/reference/mcp): the technical reference for the API and MCP. --- # Getting started ## 1. Create your workspace Sign up at [app.selda.ai](https://app.selda.ai). A **workspace** is where your go-to-market lives. Your product context, audience, campaigns, leads, and inbox in one place. ## 2. Add your product Paste your **website URL**, or describe what you built. Selda reads it and extracts your product, brand, and a first read on your market and ideal customer. ## 3. Review what Selda understood Selda shows you what it learned and the audiences it suggests. Confirm or adjust. This is where you steer who Selda targets. ## 4. Run a campaign Selda discovers fitting companies and decision-makers, researches each one, and **drafts** personalized messages. You review the drafts. ## 5. Launch Nothing sends until you approve. Hit launch, and Selda sends, follows up, and routes replies into your Sales Inbox so conversations turn into booked meetings. ## Plans & credits | Plan | Who it is for | Credits included | | --- | --- | --- | | **Free** | Seeing everything Selda does before you pay. Nothing sends. | 5 / day for 7 days, plus your first campaign on us | | **Pro** | Anyone who needs customers: the whole engine, one sending mailbox, follow-ups, the calendar, MCP and the API. | 500 / month | | **Business** | A team, or one person running several businesses: three mailboxes, seats, a shared inbox, several workspaces. | 2 000 / month | | **Enterprise** | A portfolio, an event with its own exhibitors, or your own instance. By contract. | agreed per contract | The **whole engine is in every paying plan**. What costs extra is channels (LinkedIn, WhatsApp, forums, inbound, each the same monthly price) and work (credits). The Selda bot on WhatsApp and Telegram is included in every plan, the free one included. Current prices are on the [pricing page](https://selda.ai/pricing) and in the app under **Settings → Plan and billing**. They are not repeated here, so this page cannot go stale against them. ### Credits Credits are Selda's work, drawn from one pool per organisation: researching a company, finding the decision-maker and writing a message each cost a few. **Receiving replies is always free.** Unused plan credits carry one period forward. If you need more before your plan renews, buy a **top-up pack** in **Settings → Plan and billing**, it is added to the same pool and never expires. ### Add-ons Channels are add-ons rather than part of a plan, so you pay for the ones you actually use. Each costs the same per month; the figure is on the [pricing page](https://selda.ai/pricing). - **LinkedIn**, messages and invites from your own profile, written from the same research as the email. Replies come back to your Sales Inbox. - **WhatsApp**, your workspace's own number. A person writes first and a person answers: nothing automated can send there. - **Selda Forums**, find the conversations where your people are already asking, write the reply, and watch the thread for answers after you post it. Selda never posts for you. - **Inbound intake**, your own site, forms and tools hand the lead straight to Selda. Watching a conversation costs 1 credit per thread per day. Writing the replies costs nothing. ## Next - [The Selda app](/ways-to-use/web-app), what each screen is for - [Selda in Telegram](/ways-to-use/telegram), the same machine from your phone - [Selda on your website](/ways-to-use/website-widget), the embed for WordPress and everything else - [Connect your app](/connect-your-app), the API - [MCP reference](/reference/mcp) --- # The Selda app The main way to use Selda is the app at [app.selda.ai](https://app.selda.ai). Everything else on these pages is a different door into this same machine. A **workspace** is where one go-to-market lives: one product, its Brain, its campaigns, its leads and its inbox. Run several products, or several clients, and each gets its own workspace. ## The rail on the left | | What it is for | | --- | --- | | **Home** | Where you are and what is worth doing next. | | **Campaigns** | Where a campaign is built, step by step. The centre of the product. | | **Inbox** | Replies, and the drafts Selda has prepared for them. | | **Leads** | Every company and person Selda has found or you have imported. | | **Results** | What went out, what came back. | | **Brain** | What Selda knows about your business, and the rules it must respect. | | **Settings** | Plumbing: accounts, connections, billing, team. | The dividing line between the last two is worth learning, because it is the one people get lost in. **The Brain is what Selda does for you and what it must respect.** Settings is plumbing. If you are looking for the rules about when Selda holds a message back, they are in the Brain, under *When Selda holds a send*, not in Settings. ## Building a campaign Seven screens, in order, each one a decision that is yours. Selda does the work and shows you what it found. You say what happens next. **1. The brief.** What this campaign should do, who it is for, how many companies, which language, which market. Selda proposes and you adjust, and you are never handed an empty field. If you already have your own campaign document, you can hand it over instead and Selda completes it rather than having opinions about it. **2. Companies.** Selda goes and finds them, and shows you every one with the reason it is on the list. Nothing is silently dropped: a company with no website or one that looks like a competitor is **flagged**, with the reason, and removing it is your press. If the list falls short of the number you asked for, Selda says how far short and why, and offers to look for more. It does not go looking on its own. **3. What the search looks for.** Which titles, how many people per company. On its own screen, before the search runs, so you can see what is about to be looked for. **4. People.** The decision-makers, with what Selda found about each. Same rule: shown, never quietly removed. **5. The message contract.** What every message in this campaign says: the angle, the structure, the sign-off, the ask. One place, so you are not editing thirty messages to change one thing. **6. Review.** The actual messages, one per person, each grounded in real research about that company. Edit any of them. This is the screen that decides what goes out. **7. Send.** The channels, the sending account, the settings that govern this send, and anything still standing in the way. Then the one press that sends. **Nothing sends until step 7.** Not a discovery run, not a draft, not a follow-up. The engine's whole output is drafts. ## Replies Replies land in the **Inbox** with a draft answer already written, in the same voice as the original message. You edit it or you replace it. Sending the reply is your press, the same as everything else. ## Connect what actually sends Selda sends from **your own** accounts, never from Selda infrastructure. In **Settings → Channels** you connect: - **Email**, Gmail, Outlook or any IMAP mailbox. - **LinkedIn**, on Pro and above. Both are connected once and used by every campaign in the workspace. ## Credits Credits are Selda's work, drawn from one pool per organisation: researching a company, finding the decision-maker, writing a message. **Receiving replies is always free.** The balance and the top-up packs are in **Settings → Plan and billing**. See [Getting started](/getting-started) for what each plan carries. ## Next - [Getting started](/getting-started), the first run end to end - [Selda in Telegram](/ways-to-use/telegram), the same machine from your phone - [Selda on your website](/ways-to-use/website-widget), the door that faces in - [Run it from your AI](/ways-to-use/mcp-server), Selda inside ChatGPT, Claude, Cursor or Gemini --- # Selda in Telegram A chat on your phone, and the same machine behind it as the web app. You tell it what customers you need and it prepares the campaign. Approving and sending stay in the app. **The bot can never send anything.** Not by itself and not from a button in the chat. The one function that reached the send was removed on 19.8.2026, because approving a message means seeing what will go out, and that is a screen, not a chat bubble. ## What you can do in the chat - **Ask for customers.** "we need more restaurant bookings in Tampere" becomes a campaign proposal: what Selda would look for, how many, and what it costs. It runs on your yes, never on the guess. - **Name companies.** Type them, paste a list, or send a **photo** of one (a sign, a business card, a slide, a printed list). The picture goes through the same reader the Brain uses and the companies come back as a proposal you accept or drop. - **Send a voice note.** It is transcribed first, so everything below works exactly as if you had typed it. If it cannot be heard, Selda says so instead of going quiet. - **Tell it something worth keeping.** "our best customers are chains with 3 or more locations" goes to the Brain, where the rest of Selda reads it. The bot keeps no memory of its own. - **Ask where things stand,** or ask to see the messages Selda has written. It answers in the language you write in. ## What it never does - **It does not send.** Approval is the app's screen and the confirm bar on it. - **It does not spend on its own.** Anything that costs credits is proposed first, with what it would do and what it costs, and runs only on an explicit yes. The pending proposal expires. - **It does not decide.** Same law as every other surface: Selda proposes, you pick. ## Connect it The bot is **included in every plan, the free one included**, on Telegram and on WhatsApp. There is nothing to buy for it. 1. In the app, open **Settings → Apps** and find **Selda on Telegram**. 2. Press **Connect Telegram**. Telegram's own login button links the chat in one press. If that button does not appear (it needs Telegram's cookies), press **Connect Telegram** for an 8 character code instead, and send that code to the bot in a private chat. The code is valid for 15 minutes and works once. 3. Open the chat and press **Start**. Telegram does not let a bot speak to somebody who has never opened it. Every linked chat is listed on the card with who linked it, and an **Unlink** beside it. A linked chat can start work that spends credits, so its existence is every member's business. ## Give the workspace its own bot The shared bot works, but it is one conversation for everything. A workspace can have its own bot instead, with its own name and its own thread in your chat list. This is what you hand a customer or a colleague. Telegram offers no API for creating bots, so it goes through BotFather: 1. Open [@BotFather](https://t.me/BotFather) and send `/newbot` 2. Name it, for example "Selda YourCompany", and pick a username 3. BotFather replies with a token that looks like `123456789:AA…`, copy it 4. Paste it into the card. Selda verifies the token with Telegram and registers the webhook itself. Same machine behind it, a different door. Nothing about what the bot can do changes. ## Next - [Selda on your website](/ways-to-use/website-widget), the embed that faces in - [MCP server](/ways-to-use/mcp-server), Selda inside Claude Code, Claude Desktop or Cursor - [How Selda contacts people](/how-selda-contacts-people) --- # Selda on your website Everything else in Selda goes outward. This is the one thing that faces in: a small launcher in the corner of your own site. A visitor taps it, picks what they came for, and the enquiry lands in Selda as a lead with a reply already drafted and waiting for you. One script tag. It works on any site that lets you add one. ## Why it offers choices and never chats A model answering live on your website is a sentence nobody can take back, written on a page Selda does not own, in your name. So **nothing in the request path calls a model.** Selda proposes the options once, from your Brain, when you press the button in settings. You edit them and save. From that moment a visitor only ever reads text a person approved. Free text is allowed, and it reaches you **unanswered** rather than being replied to by a machine that is guessing. A visitor is told they are using an automated form before they type anything. That line is not a setting. ## Set it up Selda Inbound is an add-on. If your workspace does not have it yet, ask support and it is switched on. 1. Open **Settings → Apps** in the app and find **Selda Inbound**. 2. Press the button that asks Selda to propose the options. It reads your Brain and suggests what people are likely to be coming for. A workspace with an empty Brain gets an honest "there is nothing here yet" and a reason, never four invented services under your company name. 3. Edit the options, the headline and the launcher label. Pick which reply channels you offer: email, WhatsApp, phone, or a booking link. 4. Optionally add a **Home** tab: a background picture, your logo and one sentence. Only tabs with something behind them are shown. 5. Save. Nothing reaches a visitor before you do. 6. Copy the embed snippet from the panel. The snippet looks like this, with your own key in it: ```html ``` Copy the exact line from the app rather than typing this one. The key is what identifies your workspace. **Paste it just before the closing `` tag.** The loader adds itself to the page body, so a footer slot is the one place that works everywhere. ## Where to paste it ### WordPress > **There is a Selda plugin for WordPress, and it does a different job.** > [Selda for WordPress](https://github.com/Selda-AI/selda-wordpress) captures the forms you already > have (Contact Form 7, WPForms, Gravity Forms, Elementor Pro) and sends the submissions to Selda. > It does not place this widget. If you want both, install the plugin and paste the snippet below; > they do not collide. Do not edit theme files directly if you can avoid it, a theme update overwrites them. **The way that survives updates:** install a header and footer snippet plugin (WPCode and "Insert Headers and Footers" are the common ones), open its settings, and paste the snippet into the **Footer** or **Body end** box. Save. That is the whole job. **With Elementor Pro:** Templates → Custom Code → add code, set Location to **End of ``**, paste, publish. **Without a plugin,** if you run a child theme: Appearance → Theme File Editor → `footer.php`, paste before ``. In a child theme only. In the parent theme the next update deletes it. **On WordPress.com** (the hosted service, not self hosted) custom scripts need the Business plan. On lower plans there is no way to add one, and no plugin changes that. Check it worked by opening your site in a private window. The launcher sits in the bottom right. ### Webflow Project settings → Custom code → **Footer code**, paste, save, publish. Custom code needs a paid site plan. ### Shopify Online Store → Themes → the three dots → Edit code → `layout/theme.liquid`. Paste before ``, save. Duplicate the theme first if you want a way back. ### Squarespace Settings → Advanced → **Code Injection** → Footer, paste, save. Code injection needs a Business plan or above. ### Wix Settings → Custom code → Add code, set it to load on all pages, place it in **Body end**. ### Framer Site settings → General → Custom code → **End of `` tag**, paste, publish. ### Next.js ```tsx // app/layout.tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ``` ### If your site has a strict Content Security Policy The widget is one script that loads one iframe, both from the origin in your snippet. Allow that origin in `script-src` and `frame-src`. Nothing else is fetched. ## What the visitor gets - A launcher in the bottom right, 380px panel on a desktop. - On a phone under 480px the panel is the whole screen, edge to edge, and it follows the keyboard rather than hiding the send button behind it. - Your CSS cannot reach into the widget and the widget's cannot reach into your page. It is an iframe on its own origin, which is what makes it safe to leave running on a site nobody at Selda has seen. - **Powered by Selda** sits at the bottom of the panel. ## Where the enquiry goes Into the same door every outside system already reports through, so there is no second lead path: ``` visitor → widget → lead (source: inbound) → draft waiting in your Sales Inbox ``` Somebody who filled in a form is waiting, so the draft is prepared immediately. **It is a draft.** The widget cannot cause a send, the same absolute as everywhere else in Selda: nothing goes out until you press it. ## Limits and safety - **60 enquiries per minute per widget,** and **6 per minute per visitor.** A refusal is an explicit "wait a moment", never a silently dropped enquiry. The ceilings exist because every enquiry drafts a reply, which spends your credits. - **Allowed origins.** You can restrict which sites may submit with your key. Leave it empty and any page carrying the snippet works, which is what you want while you are testing. - Turning the widget off in the panel stops it everywhere immediately. You do not have to touch your site again. ## Next - [Selda in Telegram](/ways-to-use/telegram), the same machine from your phone - [Connect your app](/connect-your-app), if you would rather push enquiries from your own backend - [Security and privacy](/security) --- import { Callout } from "nextra/components"; import { CopyForAI } from "../../components/CopyForAI"; # Run Selda from ChatGPT, Claude or Cursor ## What this actually is You already talk to an assistant. ChatGPT, Claude, Cursor, Gemini, whichever one you opened this week. It can write and think, and then it stops, because it has no hands: it cannot look up a company for you, cannot remember what your business sells, cannot put a message in front of a real person. This gives it hands. Paste one address into your assistant, log in once, and it can reach your Selda workspace. From then on you ask in plain words and the work happens: > "Find ten accounting firms in Gothenburg, work out who to write to at each one, and draft > something for every one of them." and your assistant does exactly that, in your workspace, with real companies and real people, and stops with ten drafts waiting for you to read. **"MCP" is the plumbing and you can forget the word.** It stands for Model Context Protocol, and all it means is a standard way for an assistant to use an outside tool. Selda speaks it, your assistant speaks it, and that is the whole of what you need to know. ## Why bother | Without it | With it | |---|---| | You open Selda in a tab, then switch back | You stay in the conversation you were already having | | You paste research from your assistant into Selda by hand | Your assistant writes it straight into the workspace | | You do one thing per click | You describe the outcome and it runs the steps | | Your assistant guesses about your business | It reads what Selda knows and works from that | **Nothing goes out without your approval. Ever.** Your assistant finds people, researches them and writes the messages, and then it stops. Every message waits for you in the Selda app until you press send yourself. This is not a setting you have to be careful with, and it is not a promise you have to take on trust: **there is no send tool.** The one function in Selda that sends appears in no list any API key can reach, on any assistant, and a test in the codebase keeps it that way. The machine-readable side of this page (tool schemas, an example client, loadable skills) lives in [Selda-AI/Selda-mcp-docs](https://github.com/Selda-AI/Selda-mcp-docs). export const PAGE_MD = `# Selda MCP server Use Selda from external AI tools (Claude, ChatGPT, Cursor) via the Model Context Protocol. The MCP server lets an AI assistant read and act on your Selda workspace: list projects and leads, run the pipeline, upload prospect material into a real campaign, draft and review messages, and check credits, all scoped to your organization. ELIGIBILITY Every plan can connect MCP with a TEST key, test mode is the full product, nothing sends for real. Real sends from your connected inboxes require a paid plan (Pro and up). Check or upgrade in Settings → Billing. CONNECT: one address, every tool https://mcp.selda.ai/api/mcp Paste it wherever the tool asks for an MCP server or a connector. It sends you to Selda to log in and pick a workspace. No key to create, copy or paste. ChatGPT Settings > Connectors > Add Claude Settings > Connectors > Add custom connector Cursor / VS Code mcp.json: { "mcpServers": { "selda": { "type": "http", "url": "https://mcp.selda.ai/api/mcp" } } } Gemini CLI ~/.gemini/settings.json: { "mcpServers": { "selda": { "httpUrl": "https://mcp.selda.ai/api/mcp" } } } ChatGPT only calls a connector exposing tools named search and fetch. Selda exposes both: search covers leads, projects and Brain items and is read-only, fetch returns one record in full by the id search gave you. Ids look like lead:... project:... brain:... CONNECT (Claude Code, scripts, CI, anything with no browser to log in with) 1. In the app: Settings > Apps > Selda MCP > create a key. 2. claude mcp add --transport http selda https://mcp.selda.ai/api/mcp --header "Authorization: Bearer sk_live_YOUR_KEY_HERE" 3. Ask your assistant to "list my Selda projects" to confirm the connection. Check the address itself with: curl https://mcp.selda.ai/api/mcp (no key needed) WHAT YOU CAN DO - "List my Selda projects." - "Look up Example Oy and tell me who to reach in marketing." - "Create a campaign from this folder of prospect research I already wrote, one file per company, upload the PDFs and Markdown, then find decision-makers and draft messages." - "Find 20 SaaS founders in Finland and add them as leads to my main project." - "Check the status of that campaign run, how many companies, how many drafts." - "Draft a first-touch message for this lead based on their website." - "Show me campaign stats, replies and meetings, not just sends." - "How many credits do I have left?" - "Add these companies from this page as leads: ." Nothing sends automatically, every tool that touches a campaign produces drafts you approve inside the Selda app. There is no MCP tool that sends a message. Full tool list and details: https://docs.selda.ai/reference/mcp`; Prefer to hand your assistant a ready-made skill? Copy the whole [**Selda skill**](/ways-to-use/selda-skill) in one click, then connect the MCP server below. ## Copy this ``` https://mcp.selda.ai/api/mcp ``` That is the whole setup. Nothing to install, no key to create. Find where your tool asks for an "MCP server" or a "connector", paste that address, and log in with the Selda account you already use. Then pick which workspace it may reach. Below is where that box is in each tool. ### ChatGPT 1. **Settings → Connectors → Add** 2. Paste `https://mcp.selda.ai/api/mcp` 3. Log in when it sends you to Selda, and pick your workspace 4. Ask it: **"List my Selda projects"** If step 4 answers with your workspaces, you are done. ChatGPT only calls a connector that exposes tools named `search` and `fetch`; Selda exposes both, so there is nothing else to set up. ### Claude (claude.ai or the desktop app) 1. **Settings → Connectors → Add custom connector** 2. Paste `https://mcp.selda.ai/api/mcp` 3. Log in, pick your workspace 4. Ask it: **"List my Selda projects"** ### Cursor (also Windsurf, Zed, VS Code) 1. **Settings → MCP → Add new MCP server**, or write the file yourself: `~/.cursor/mcp.json`, or `.vscode/mcp.json` in the project for VS Code 2. Paste this in: ```json { "mcpServers": { "selda": { "type": "http", "url": "https://mcp.selda.ai/api/mcp" } } } ``` 3. Restart the editor, log in when it asks 4. Ask it: **"List my Selda projects"** ### Gemini CLI 1. Open `~/.gemini/settings.json` 2. Paste this in: ```json { "mcpServers": { "selda": { "httpUrl": "https://mcp.selda.ai/api/mcp" } } } ``` 3. Restart `gemini`, log in when it asks 4. Ask it: **"List my Selda projects"** ### Claude Code, scripts, a server, CI Anything with no browser cannot do the login step, so it needs a key instead. 1. In the app: **Settings → Apps → Selda MCP → create a key**. The full value is shown once, copy it 2. Run: ```bash claude mcp add --transport http selda https://mcp.selda.ai/api/mcp \ --header "Authorization: Bearer PASTE_YOUR_KEY" ``` 3. Ask it: **"List my Selda projects"** The key goes in a header, never in the URL, so it stays out of shell history and proxy logs. The same screen in the app tells you whether a call has actually arrived on that key; it only says connected once one has. ### Something else Any tool that speaks MCP over HTTP works. Give it `https://mcp.selda.ai/api/mcp` and either let it log you in, or send `Authorization: Bearer sk_live_...` yourself. ### If it does not connect Run this. It needs no key and no login: ```bash curl https://mcp.selda.ai/api/mcp ``` You should get back `{"name":"selda","version":"...","status":"ok"}`. If you do, the address is right and the problem is in your tool's config, not in Selda. (The version moves; `status: ok` is the part that matters.) ## Eligibility [#eligibility] **Every plan can connect, in test mode.** Creating a key on a free workspace mints a **test key**: the full product in test mode, MCP included, and nothing sends for real. Real sends from your connected inboxes require a paid plan (**Pro** and up). Check or upgrade in **Settings → Billing** in the [app](https://app.selda.ai). ## How the authentication actually works Two ways in, one thing underneath: **every call carries a Selda API key**, and the key is what says which organisation you are. ### The OAuth way (no key to copy) You add the address, and the client does the rest: 1. It calls the server without credentials. Selda answers `401` with a `WWW-Authenticate` header pointing at `/.well-known/oauth-protected-resource`, which points at the authorization server (`https://api.selda.ai`). This is how the client discovers the flow without being told. 2. It **registers itself** (`POST /oauth/register`, RFC 7591). No human step, no client id to create by hand. 3. It sends your browser to `https://app.selda.ai/oauth/authorize`. You log in with the account you already use, and **approve one workspace**. 4. It exchanges the resulting code for an access token (`POST /oauth/token`, Authorization Code with PKCE, `S256`). **The access token is an `sk_live_…` key under the hood.** Same organisation scoping, same list, same revoke button in **Settings → Apps → Selda MCP**. OAuth is a way to mint one without anybody copying a string, not a second permission system. ### The static key way Create a key in **Settings → Apps → Selda MCP** and put it in a header. The full value is shown once. ``` Authorization: Bearer sk_live_... ``` The key travels in a header, never in the URL, so it stays out of shell history and proxy logs. ### What a key can and cannot reach - **The organisation is resolved from the key.** You never send an `orgId`, and a key made in one organisation cannot see another one's workspaces. That is enforced server side on every call, not by the client asking nicely. - **Scopes:** `read` for queries, `write` for mutations, `pipeline` for engine runs. A key without the scope gets `403` and a sentence saying which one is missing. - **Test keys (`sk_test_…`) cannot spend or send.** Every plan can connect with one. Real sends from your connected inboxes need Pro or above. - **Keys are stored hashed.** Selda cannot show you an existing key again, only replace it. - **Revoke** in **Settings → Apps → Selda MCP**. The same screen tells you whether a call has ever arrived on that key, so "connected" means a request was actually seen, not that a string was pasted somewhere. - **No key of any kind can send a message.** There is no MCP tool for it, on any plan, with any scope. ## What you can do with it **Everything, with one exception.** Connected, your assistant runs the whole product: it lists and opens your workspaces, reads and corrects leads, writes the Brain that every message is built from, starts the engine, watches a run, reads what came back, and drafts replies on real threads in your inbox. Sixty-two tools, and they are the same functions the app's own buttons call. This is not a read-only view of Selda, it is Selda with a different front end. The exception is the send. There is no send tool, on any client, with any key. That is not a setting somebody could turn on: the one function that sends appears in no registry an API key can reach, and a test pins that. So the most an assistant can do is put a finished draft in front of you, which is exactly as far as it should get. Once connected, these prompts produce real work in your Selda workspace: - **"List my Selda projects."**, see your workspaces and get the `projectId` everything else needs. - **"Look up Example Oy and tell me who to reach in marketing."**, enriched company info and matched decision-makers, no campaign started. - **"Create a campaign from this folder of prospect research, upload the files, find decision-makers, draft messages."**, one file per company (PDF, Markdown, JSON); lands as a real, reviewable campaign in the app, exactly like dragging the folder in yourself. - **"Find 20 SaaS founders in Finland and add them as leads to my main project."**, runs the discovery pipeline and stores drafted outreach. - **"Check the status of that campaign run."**, phase, companies found, contacts resolved, messages drafted. - **"Draft a first-touch message for this lead based on their website."**, a personalized draft grounded in real research. - **"Check my credits."**, remaining balance and plan. - **"Show campaign stats for my Q3 campaign."**, sent, replies, meetings, the numbers that matter. - **"Add these companies from this page as leads."**, paste a URL or list; known contacts get added directly. Nothing sends automatically. Every tool that touches a campaign produces **drafts** you approve inside the Selda app, there is no MCP tool that sends a message. ## Build your own pipeline Everything above is one question at a time. The step after that is a sequence you repeat, and you build it the same way: by describing it, not by writing code. **Start with what your assistant already knows about you.** This is the part people miss. If you have been using ChatGPT for a while, it has already been told about your business: in a project, in custom instructions, in an uploaded deck, or just across a lot of conversations. That context does not have to be typed into Selda a second time. Hand it over: > "You already know what we do from our earlier conversations. Write that into the Selda Brain: > what we sell, who we sell it to, the two customers we can name, and the one thing we must never > claim about ourselves." The Brain is what every message is built from, so this is the step that decides whether the outreach sounds like you or like a template. If your assistant does not know enough yet, point it at the source instead: > "Read our website and our pricing page, then put what you learn into the Selda Brain." Anything invented here ends up in a real message to a real person, so it has to be facts. That is also why the Brain has an `avoid` type: the things Selda must never claim are worth writing down as explicitly as the things it may. **Then hand it the research you already do.** If you analyse prospects somewhere else, in a spreadsheet, in a folder of PDFs, in another assistant, that work does not have to be repeated: > "Here are eleven PDFs, one per company. Load them into Selda as a campaign, find the > decision-maker at each, and draft from what is in the file rather than crawling the site." That lands as a real campaign in the app, the same as if you had dragged the folder in yourself. **Then let things arrive on their own.** When someone fills in your form or an ad gets answered, your own code or automation can tell Selda, and it picks the person up from there: > "When a signup arrives, add them to Selda with whatever we know, join them to the onboarding > campaign, and draft the first message." **If you build software, this is where it gets interesting.** The thing your product already knows about a user, that they signed up, that they hit a limit, that they finished a trial without buying, is exactly the thing that decides what to say to them and when. You do not need a separate sales tool watching from outside; your own app can tell Selda directly, and the outreach is written from the actual event rather than from a guess about it. Ask your assistant to wire it: > "Add a call to Selda in our signup handler: send the email, the company domain and what plan they > picked, join them to the onboarding campaign, and draft the first message." Your assistant writes that code against the same API, in your codebase, and from then on the product drives the outreach. The draft still waits for you. **Then keep going.** Ask for the run's status, read what came back, correct a lead that is wrong, draft the reply to someone who answered. It is all one conversation, and none of it sends. The order that works: **facts in → material in → run → read → correct → draft the reply.** Each step is a sentence, and you can stop at any of them. → Longer patterns people actually build: [What people build with it](/ways-to-use/mcp-use-cases) → Full tool list and details: [MCP server reference](/reference/mcp). --- # Prompts that do something Copy these. They work in the app's own chat, in the Telegram bot, and in any AI client connected to the [MCP server](/ways-to-use/mcp-server). Where a prompt only works in one place, it says so. Replace anything in `` with your own. > **Nothing on this page sends a message.** Prompts that mention outreach produce drafts. The send is > a human press in the app, and no phrasing changes that. --- ## Set the workspace up **Teach it what you sell.** Selda writes from what it knows, so this is the highest-value five minutes you will spend. ``` Here is what we sell: . It costs . Our customers are usually . The thing that makes them buy is . Save that to the Brain. ``` ``` Read and tell me what you understood about our product, our customers and what we are actually selling. Do not write anything yet. ``` **Say what you will never claim.** This one prevents more bad messages than any positive instruction. ``` Add to the Brain: we never claim , we do not work with , and we never promise delivery times. ``` --- ## Find customers ``` Find <20> in that , and tell me why each one is on the list before you research them. ``` ``` I want meetings with at companies that . Propose three angles, tell me which you would pick and why, and wait for me to choose. ``` **Name the companies yourself** when you already know who you want. This is cheaper and better than a search, because there is nothing to guess. ``` Add these as leads and find the right person at each: ``` ``` Take the companies on this page and add them: ``` **In Telegram** the short forms work: send a list of names, or a photo of one (a sign, a business card, a slide), and Selda comes back with a proposal to accept or drop. --- ## Use research you already did The highest-quality outreach Selda writes is the outreach it did not have to guess at. If you or another AI tool already wrote up a company, hand that over. ``` Here is my write-up of . Use it as the research, do not crawl the site, and draft the first message from it: ``` ``` I have one file per company in . Upload them, make one campaign of it, find the decision-makers and draft the messages. ``` Selda stays faithful to what you wrote and invents nothing beyond it. A vague write-up produces a vague message, so keep it factual. --- ## Check on things ``` What is the status of that run? How many companies, how many drafts, and is it waiting for me? ``` ``` Show me the campaign stats: sent, replies and meetings, not just sends. ``` ``` How many credits do we have left, and what would one more run of <30> cost? ``` ``` Which leads replied and nobody has answered yet? ``` > **On a run, always ask "is it waiting for me?"** The engine deliberately stops at the company list > and waits for a person. That is not a hang. --- ## Handle replies ``` Draft a reply to 's message. Same voice as the original, answer the question they actually asked, and do not promise anything that is not in the Brain. ``` ``` Read the last <10> replies and tell me which ones are worth my time, with the reason. ``` --- ## Ask why, not just what Selda's judgement is meant to be visible. These prompts pull it out. ``` Why did you drop ? Show me the reason, and put it back if the reason is that its website is bad. ``` ``` Why is this message going to this person? What do you actually know about them? ``` ``` This list is shorter than I asked for. How far short, and why, company by company? ``` --- ## Have a coding agent build the integration Paste this into Claude Code, Cursor or Lovable, in the project you want to connect: ``` Read https://docs.selda.ai/llms-full.txt Then add Selda to this project: when , call events.ingest with the person's details and what happened. Use an idempotency key derived from the submission so a retry replays instead of creating a second lead. Put SELDA_API_KEY and SELDA_PROJECT_ID in the environment, never in the code. Start with a test key. ``` For a webhook receiver: ``` Read https://docs.selda.ai/connect-your-app/webhooks Add an endpoint that receives Selda webhooks. Verify X-Selda-Signature against the raw body BEFORE parsing it, reject anything that fails, and handle reply.received by . ``` The app also has a **"Copy setup prompt"** button that fills in your own product and project id. --- ## What to say when it goes wrong ``` That message reads like AI. Tell me which line did it and rewrite just that one. ``` ``` You used a fact I cannot verify. Show me where it came from, and drop it if the source is not real. ``` ``` Stop. Do not spend anything. Tell me what you were about to do and what it costs first. ``` The last one should never be necessary. Anything that spends is supposed to propose first and wait for your yes. If something spent without asking, tell us: support@selda.ai. --- import { Callout } from "nextra/components"; import { CopyForAI } from "../../components/CopyForAI"; # Selda skill Give this to Claude or ChatGPT as a skill and your assistant knows how to drive your Selda workspace: list projects, find and add leads, run the go-to-market pipeline, draft and review outreach, and check credits, all scoped to your org by your API key. **Every plan can connect, in test mode.** A free workspace gets a test key (test mode, nothing sends for real); a live key requires a paid plan (**Pro** and up). See [Eligibility](/ways-to-use/mcp-server#eligibility) to check or upgrade. ## One-click: give it to your assistant Copy the whole skill and paste it into Claude or ChatGPT. Then connect the [MCP server](/ways-to-use/mcp-server) so the assistant can actually act. export const SKILL = `You are wiring an AI assistant into a user's Selda workspace. Selda is a go-to-market engine for builders. It finds who to reach, works out what to say from real research, and runs the channels that get replies and meetings, email, LinkedIn, and communities. The app lives at https://app.selda.ai (never selda.city or any other domain). All calls are org-scoped by the user's API key, you never send orgId or userId. GOLDEN RULES 1. Always call selda_list_projects first to get a projectId before anything else. 2. Nothing sends automatically. The pipeline and message tools produce DRAFTS; a human approves and sends inside the Selda app. Never imply Selda blasts email. 3. Precision over volume. A few high-fit leads with a real reason to reach out, not a big list. 4. Direct add vs. discovery. If the user hands you specific companies/people (a URL, a list), add them directly with selda_add_lead. Use selda_run_pipeline only for open-ended searches like "find SaaS founders in Finland". 5. The pipeline costs credits and returns leads + drafted messages after a few minutes. Poll selda_list_leads to see results; check selda_credits if runs fail. TOOLS - selda_list_projects, get workspaces and the projectId everything else needs - selda_get_project, a workspace's business context, ICP, settings - selda_list_leads, a project's leads (contact, company, fit, status) - selda_get_lead, full lead detail: research, fit, why-good-lead, angle - selda_add_lead, add a known company/person directly (dedupes by email) - selda_update_lead, move status (new→contacted→responded→qualified) or notes - selda_list_campaigns, a project's campaigns - selda_get_campaign, campaign detail - selda_campaign_stats, sent / delivered / opened / replied / bounced - selda_list_messages, sent and received messages for a project - selda_get_thread, the full email thread with one lead - selda_run_pipeline, run the engine: find leads → research → personalized drafts - selda_credits, check credit balance and plan TYPICAL FLOWS - Explore: selda_list_projects → selda_get_project → selda_list_leads - Add known prospects: selda_list_projects → selda_add_lead (per company) → tell the user drafts are ready to review and approve in the app - Discovery: selda_list_projects → selda_run_pipeline with a clear brief (who + where + why now) → wait → selda_list_leads to read results - Report: selda_list_campaigns → selda_campaign_stats → summarize replies and meetings, not just sends VOICE When you draft or summarize outreach, keep it human: one concrete idea, a real observation about the prospect, no template feel, no "I hope this email finds you well". Selda wins by understanding first, reflect that in every message.`; ```text You are wiring an AI assistant into a user's Selda workspace. Selda is a go-to-market engine for builders. It finds who to reach, works out what to say from real research, and runs the channels that get replies and meetings, email, LinkedIn, and communities. The app lives at https://app.selda.ai (never selda.city or any other domain). All calls are org-scoped by the user's API key, you never send orgId or userId. GOLDEN RULES 1. Always call selda_list_projects first to get a projectId before anything else. 2. Nothing sends automatically. The pipeline and message tools produce DRAFTS; a human approves and sends inside the Selda app. Never imply Selda blasts email. 3. Precision over volume. A few high-fit leads with a real reason to reach out, not a big list. 4. Direct add vs. discovery. If the user hands you specific companies/people (a URL, a list), add them directly with selda_add_lead. Use selda_run_pipeline only for open-ended searches like "find SaaS founders in Finland". 5. The pipeline costs credits and returns leads + drafted messages after a few minutes. Poll selda_list_leads to see results; check selda_credits if runs fail. TOOLS - selda_list_projects, get workspaces and the projectId everything else needs - selda_get_project, a workspace's business context, ICP, settings - selda_list_leads, a project's leads (contact, company, fit, status) - selda_get_lead, full lead detail: research, fit, why-good-lead, angle - selda_add_lead, add a known company/person directly (dedupes by email) - selda_update_lead, move status (new→contacted→responded→qualified) or notes - selda_list_campaigns, a project's campaigns - selda_get_campaign, campaign detail - selda_campaign_stats, sent / delivered / opened / replied / bounced - selda_list_messages, sent and received messages for a project - selda_get_thread, the full email thread with one lead - selda_run_pipeline, run the engine: find leads → research → personalized drafts - selda_credits, check credit balance and plan TYPICAL FLOWS - Explore: selda_list_projects → selda_get_project → selda_list_leads - Add known prospects: selda_list_projects → selda_add_lead (per company) → tell the user drafts are ready to review and approve in the app - Discovery: selda_list_projects → selda_run_pipeline with a clear brief (who + where + why now) → wait → selda_list_leads to read results - Report: selda_list_campaigns → selda_campaign_stats → summarize replies and meetings, not just sends VOICE When you draft or summarize outreach, keep it human: one concrete idea, a real observation about the prospect, no template feel, no "I hope this email finds you well". Selda wins by understanding first, reflect that in every message. ``` ## Next step The skill tells the assistant *what to do*. To let it actually act on your workspace, connect the [Selda MCP server](/ways-to-use/mcp-server) with your API key. Once connected, try: *"List my Selda projects, then find 20 SaaS founders in Finland and add them as leads."* --- import { Callout } from "nextra/components"; import { CopyForAI } from "../../components/CopyForAI"; # What people build with it The MCP server is not a second Selda. It is the same engine with a different front door, so the useful question is not "what can it do" but "what does it let you stop doing by hand". Seven patterns, each one built from tools that exist today. export const PAGE_MD = `# What people build with the Selda MCP server Seven patterns, each built from tools that exist today. The MCP server is the same engine as the app with a different front door. RULE THAT SHAPES ALL OF THEM Nothing in the MCP surface sends. There is no send tool and there never will be. Every pattern below ends in drafts a human approves in the Selda app. That is what makes it safe to let a model drive. 1. ASK ABOUT YOUR OWN COMPANY IN THE TOOL YOU ALREADY USE (no code) Connect once from the app, then ask questions. "Who should I talk to at Example Oy and what do we know about them?" "What happened in my campaigns this week?" "Who replied and hasn't been answered?" "Remember that we now sell the maintenance package too." projects.get, leads.list, messages.byLead, campaigns.stats, knowledge.get, brain.add This is the pattern most people end up using. 2. YOUR OWN AI TOOL DRIVES THE CAMPAIGN Write the brief in ChatGPT, Claude or Cursor, hand it to Selda, get the work back, improve it where you already work. engine.start the brief becomes a run: find companies, research, fit, hook, draft runs.status poll; awaitingHuman tells you when it is waiting on a person, not on Selda (first stop: the company list, confirmed in the app) runs.leads every company found, its contact, and the message drafted for it drafts.update rewrite a subject or body after improving it in your tool Then approve and send in the app. Typical run: ~10 leads in a few minutes of engine time, plus however long the company list waits for you. 3. YOUR PRODUCT'S SIGNUPS BECOME LEADS Someone registers, books a demo or hits a pricing page. Your backend tells Selda, and the person is in the pipeline without anyone copying a row. events.ingest report that something happened outside Selda leads.add add the company and contact directly, with research you already have leads.addBatch the same for many at once 4. YOUR CRM STAYS THE SYSTEM OF RECORD Selda does the finding, researching and writing; your CRM keeps owning the truth. webhooks.create register an endpoint for reply.received, meeting.booked, campaign.completed, lead.added, message.sent, credits.low leads.list read the pipeline back on your schedule messages.byLead the whole thread with one person, both directions 5. RESEARCH YOU ALREADY DID You have a folder of analyses. Selda uses them as the authoritative source instead of crawling the sites again. POST /mcp/material/upload one file per company (PDF, Markdown, JSON, text) material.import folder becomes a campaign plus a company list, then stops Pass analysis on leads.add to attach your own research to a single company. 6. REPLIES HANDLED IN YOUR OWN STACK webhooks.create reply.received fires the moment an answer lands replies.classify interested, not now, wrong person, unsubscribe replies.draft an answer written by the same engine as the opener The reply conversation stays yours. Selda drafts; you decide and send. 7. ONE KEY PER CLIENT (AGENCIES AND PORTFOLIOS) A key created from a workspace is bound to it. Connect one client's workspace to one tool without exposing the others in the same organisation. projects.list lists only the workspace the key belongs to any other call 403 workspace_not_allowed if aimed elsewhere Keys created before 8 August 2026 have no binding and remain organisation-wide. ZAPIER, MAKE, N8N They do not speak MCP and do not need to. Same HTTP API underneath. Zapier -> Selda Webhooks by Zapier, POST https://api.selda.ai/mcp/mutate header Authorization: Bearer sk_..., body {"fn":"leads.add","args":{...}} Selda -> Zapier webhooks.create pointed at a Catch Hook URL; plain JSON, event in a header Outbound webhooks are signed, verify the signature if the receiver is public. WHAT IT COSTS Test keys are free and are not gated on a plan or an invoice: build the whole integration, push data in, read it back, edit drafts, on any plan. The calls that spend real money or reach real people need a live workspace on a paid plan: company.lookup, leads.enrich, leads.enrichBatch, connectors.sync, engine.start. `; **Nothing here sends.** There is no send tool in the MCP surface and there never will be. Every pattern below ends in **drafts a human approves** in the Selda app. That is precisely what makes it safe to let a model drive the rest. ## 1. Ask about your own company, in the tool you already use **For:** anyone who is not a developer and never wants to be. You connect Selda to ChatGPT or Claude once, from the app, with no key to copy. After that your assistant can see what your company actually knows, and answer from that instead of guessing. - *"Who should I be talking to at Example Oy, and what do we already know about them?"* - *"What happened in my campaigns this week?"* - *"Who replied and hasn't been answered yet?"* - *"Remember that we now also sell the maintenance package"*, goes into the Brain, and every future message is written knowing it The tools behind these are `projects.get`, `leads.list`, `messages.byLead`, `campaigns.stats`, `knowledge.get` and `brain.add`. You never type a tool name; you ask a question. This is the pattern most people end up using, and it needs no code at all. ## 2. Your own AI tool drives the campaign **For:** anyone who already works in ChatGPT, Claude or Cursor and does not want a second place to think. You write the brief where you are. Selda does the finding and the writing. The work comes back to you, you improve it, and it goes back, without either side becoming a copy-paste job. | step | tool | |---|---| | the brief becomes a run | `engine.start` | | poll it | `runs.status`, `awaitingHuman` names what it is waiting for | | confirm the company list | in the app: Selda proposes the companies and the decision-maker roles, you decide | | read every company, contact and draft | `runs.leads` | | write your improved version back | `drafts.update` | The run **stops** once it has the company list (`awaiting_profile`) and waits for you to confirm or edit it. That gate is deliberately not something an API key can press, Selda proposes, you decide, so a script should treat `awaitingHuman` as "your turn", never as a hang and never as a finish. Then you approve and send in the app. A run of ten leads takes a few minutes of engine time; asking for two is not faster, because discovery costs the same either way. ## 3. Your product's signups become leads **For:** anyone with their own software, where the interesting event happens outside Selda. Someone registers, books a demo, or reads the pricing page three times. Your backend says so, and that person is in the pipeline, no export, no weekly copy-paste. - `events.ingest`, report that something happened outside Selda - `leads.add` / `leads.addBatch`, add the company and contact directly, with `analysis` carrying research you already have This is the direction people usually mean by "inbound". It needs no new integration: it is one call from your backend. ## 4. Your CRM stays the system of record **For:** teams who already have a CRM and are not migrating to a new one. Selda finds, researches and writes. Your CRM keeps owning the truth. The two stay in step through events rather than a sync. - `webhooks.create`, register your endpoint for `reply.received`, `meeting.booked`, `campaign.completed`, `lead.added`, `message.sent`, `credits.low` - `leads.list`, `messages.byLead`, read the pipeline and the full thread on your own schedule ## 5. Research you already did **For:** anyone with a folder of analyses, notes or PDFs about their prospects. Selda uses your material as the authoritative source instead of crawling the sites again, your judgement about a company beats a fresh scrape of its homepage. - `POST /mcp/material/upload`, one file per company; the path groups them (`boreo/analyysi.pdf`) - `material.import`, the folder becomes a campaign and a company list, **and stops there** for review To attach research to a single company instead, pass `analysis` on `leads.add`. ## 6. Replies handled in your own stack **For:** anyone whose conversations should live where their team already is. - `webhooks.create`, `reply.received` fires the moment an answer lands - `replies.classify`, interested, not now, wrong person, unsubscribe - `replies.draft`, an answer written by the same engine that wrote the opener, with the same guards The reply conversation stays yours by default. Selda drafts; you decide and send. ## 7. One key per client **For:** agencies and operators running several businesses in one organisation. A key created from a workspace is **bound to that workspace**. You can connect one client to one tool without exposing the others. - `projects.list` returns only the workspace the key belongs to - any call aimed elsewhere is refused with `403 workspace_not_allowed` Keys created before **8 August 2026** carry no binding and stay organisation-wide, so nothing you already connected changes. Create a new key from a workspace to scope it. ## Zapier, Make and n8n A common question, and the answer has two halves. **These platforms do not speak MCP.** MCP is a protocol for AI assistants, Claude, ChatGPT, Cursor, and Zapier is not one of them. So Selda's tools do not appear in Zapier's tool list. **They do not need to.** Everything the MCP server can do is the same HTTP API underneath, and that is exactly what an automation platform is good at: | direction | how | |---|---| | Zapier → Selda | **Webhooks by Zapier**, POST to `https://api.selda.ai/mcp/mutate` with header `Authorization: Bearer sk_…` and body `{"fn":"leads.add","args":{…}}` | | Selda → Zapier | `webhooks.create` pointed at a **Catch Hook** URL; Selda POSTs plain JSON with the event in a header | So a Zap like *"new row in Google Sheets → add a lead in Selda"* or *"Selda got a reply → post it to Slack"* works today, with no integration to install. The same is true of Make and n8n. The one thing to know: a webhook out of Selda is signed. Verify the signature header if the receiving side is public, exactly as you would with Stripe. ## What it costs **Test keys are free**, and are not gated on a plan or an invoice. You can build the entire integration, push in leads, knowledge and events, read everything back and edit drafts, on any plan, including free. The calls that spend real money or reach real people need a live workspace on a paid plan: `company.lookup`, `leads.enrich`, `leads.enrichBatch`, `connectors.sync` and `engine.start`. Each one states its own reason in `liveOnlyReason` in the [capability manifest](https://api.selda.ai/mcp/capabilities), and the refusal says it in plain words rather than a code: ```json { "code": "live_key_required:paid_engine_run", "message": "This runs Selda's discovery engine, which spends real money searching, crawling and researching companies, so it needs a live key on a paid plan." } ``` → Set it up: [MCP server](/ways-to-use/mcp-server) · Full tool list: [reference](/reference/mcp) --- import { Callout } from "nextra/components"; # Connect your app One endpoint pattern, one auth header, no SDK. Works from any backend in any language. ## 1. Get a key **Settings → Apps → Selda MCP** in the app. The `sk_live_…` value is shown once. Start with an `sk_test_…` key. Test keys reach everything and **cannot send anything**, so you can build the whole integration before any of it is real. ``` SELDA_API_KEY=sk_live_... SELDA_PROJECT_ID=... ``` ## 2. Make one call ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.add", "args": { "projectId": "'"$SELDA_PROJECT_ID"'", "company": "Example Oy", "email": "owner@example.fi", "companyDomain": "example.fi", "source": "my-app" } }' ``` ```json { "value": { "leadId": "...", "duplicate": false } } ``` Selda dedupes by email, so re-running is safe. That is the whole integration for most people. ## 3. That is the shape of everything else Three endpoints, one body format. Change the `fn` and you have a different call. ``` POST https://api.selda.ai/mcp/mutate change something POST https://api.selda.ai/mcp/query read something POST https://api.selda.ai/mcp/run start work that takes a while Authorization: Bearer sk_live_... { "fn": "", "args": { ... } } ``` Every `fn` there is: [function reference](/connect-your-app/reference). ## The same API, as URLs If your tooling expects a URL per operation rather than an `fn` in a body, the same calls also have REST paths: ``` GET https://api.selda.ai/v1/leads POST https://api.selda.ai/v1/runs/{runId}/confirm-companies Authorization: Bearer sk_live_... ``` It is not a second API and it cannot reach anything the RPC form refuses: a path resolves to an `fn`, and the request is answered by the same dispatcher, with the same key check, scope check, plan gate, workspace binding and sandbox rule. Both lists are generated from the same registry, so they cannot drift apart. [Every path](/reference/rest) · [OpenAPI 3.1 document](/openapi.json) **The REST paths are not on production yet.** `api.selda.ai/mcp/*` is live; `api.selda.ai/v1/*` answers `404 No matching routes found` until the deploy that carries them lands. Build against the RPC form above if you are integrating today, it is the same functions and the same arguments, so moving to a path later changes only the URL. --- ## Which pattern do you need? | You want to | Do this | | --- | --- | | Send leads from your backend, a cron job or a form | **Push.** `leads.add`, or `events.ingest` when something happened rather than somebody exists. [Examples](/connect-your-app/examples) | | Show your pipeline in your own dashboard | **Read.** `campaigns.stats`, `leads.list`, `messages.list`. [Reference](/connect-your-app/reference#read-post-mcpquery) | | React when a reply arrives, without polling | **Webhooks.** Register a URL, Selda POSTs a signed payload. [Webhooks](/connect-your-app/webhooks) | | Let Selda fetch from you instead | **Connectors.** Selda polls your JSON endpoint on a schedule. [Below](#let-selda-poll-you) | | Drive Selda from Claude, ChatGPT or Cursor | **MCP**, not this. [MCP server](/ways-to-use/mcp-server) | | Put Selda on a WordPress site | **The plugin**, not this. [Selda for WordPress](https://github.com/Selda-AI/selda-wordpress) | > **Using an AI tool to write the integration?** Hit **"Copy setup prompt"** in the app and paste it > into Claude Code, Cursor or Lovable with your product. > > Or point it at these docs as plain text: **`https://docs.selda.ai/llms-full.txt`** is every page in > one file, and [`llms.txt`](https://docs.selda.ai/llms.txt) is the index. Both are regenerated on > every build, so they cannot drift from the pages. --- ## Push your own research in If you already researched the company, pass it in `analysis`. Selda uses it as the **authoritative research grounding**: it writes the outreach **from your analysis** instead of crawling from scratch, and labels the draft **"Based on your analysis"** in the app. ```js await fetch("https://api.selda.ai/mcp/mutate", { method: "POST", headers: { Authorization: `Bearer ${process.env.SELDA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ fn: "leads.add", args: { projectId: process.env.SELDA_PROJECT_ID, company: "Example Oy", companyDomain: "example.fi", analysis: "Example Oy builds industrial IoT sensors. Opened a Munich office in Q1 2026 and is hiring " + "field engineers, a clear expansion signal to lead with.", source: "claude", }, }), }); ``` > **Faithful by design.** The `analysis` text is treated as grounded source, so the message can > reference its facts, but Selda never fabricates beyond it. Keep it factual and the outreach stays > accurate. Via MCP the same lands as the `analysis` parameter on `selda_add_lead`. --- ## Let Selda poll you Selda fetches your HTTP JSON endpoint on a schedule and syncs the records as leads. Useful when pushing from your side is the awkward direction. Register it in **Settings → Apps → Connect your API**, or: ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fn": "connectors.create", "args": { "projectId": "...", "name": "My App", "url": "https://api.example.com/customers", "authHeader": "Bearer sk_...", "fieldMap": { "company": "companyName", "email": "ownerEmail", "companyDomain": "website", "notes": "city" } } }' ``` Your endpoint returns a JSON array. Selda maps fields via `fieldMap`, dedupes by email, and polls on a schedule. --- ## Nothing here sends Not a lead you push, not an event you ingest, not a draft Selda writes in response. A script can build a campaign all the way up to drafted messages; a human presses send, in the app. There is no function that launches sends and no flag that changes it. ## Next - [Examples](/connect-your-app/examples), copy-paste in curl, Node, Python and PHP - [Webhooks](/connect-your-app/webhooks), find out when something happens - [Function reference](/connect-your-app/reference), every `fn` and its arguments - [Runnable code on GitHub](https://github.com/Selda-AI/selda-examples) *Source of truth: `convex/mcpApi.ts`. See [Security](/security) for key and webhook secret handling.* --- import { Tabs } from "nextra/components"; # Examples Every example on this page is a complete, runnable call. Set two variables and paste. ``` SELDA_API_KEY=sk_test_... # Settings → Apps → Selda MCP. Test keys cannot send anything. SELDA_PROJECT_ID=... ``` All of it is also a repository you can clone: [Selda-AI/selda-examples](https://github.com/Selda-AI/selda-examples). --- ## Add one lead The most common call there is. Selda dedupes by email, so running it twice is safe. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.add", "args": { "projectId": "'"$SELDA_PROJECT_ID"'", "company": "Example Oy", "email": "owner@example.fi", "companyDomain": "example.fi", "source": "my-app" } }' ``` ```js const res = await fetch("https://api.selda.ai/mcp/mutate", { method: "POST", headers: { Authorization: `Bearer ${process.env.SELDA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ fn: "leads.add", args: { projectId: process.env.SELDA_PROJECT_ID, company: "Example Oy", email: "owner@example.fi", companyDomain: "example.fi", source: "my-app", }, }), }); const { value } = await res.json(); ``` ```python import json, os, urllib.request req = urllib.request.Request( "https://api.selda.ai/mcp/mutate", data=json.dumps({ "fn": "leads.add", "args": { "projectId": os.environ["SELDA_PROJECT_ID"], "company": "Example Oy", "email": "owner@example.fi", "companyDomain": "example.fi", "source": "my-app", }, }).encode(), headers={ "Authorization": f"Bearer {os.environ['SELDA_API_KEY']}", "Content-Type": "application/json", }, ) with urllib.request.urlopen(req) as res: value = json.load(res)["value"] ``` ```php $ch = curl_init( 'https://api.selda.ai/mcp/mutate' ); curl_setopt_array( $ch, array( CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array( 'Authorization: Bearer ' . getenv( 'SELDA_API_KEY' ), 'Content-Type: application/json', ), CURLOPT_POSTFIELDS => json_encode( array( 'fn' => 'leads.add', 'args' => array( 'projectId' => getenv( 'SELDA_PROJECT_ID' ), 'company' => 'Example Oy', 'email' => 'owner@example.fi', 'companyDomain' => 'example.fi', 'source' => 'my-app', ), ) ), ) ); $value = json_decode( curl_exec( $ch ), true )['value']; ``` **The response always has the same four fields**, so your code never has to branch on which path it took: ```json { "value": { "leadId": "k57f...", "duplicate": false, "blocked": false, "reason": null } } ``` `blocked: true` means the address asked Selda to stop. Nothing was created, and pushing it again will not change that. Read `reason` and move on. --- ## Add many at once ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.addBatch", "args": { "projectId": "'"$SELDA_PROJECT_ID"'", "leads": [ { "company": "Example Oy", "email": "owner@example.fi", "companyDomain": "example.fi" }, { "company": "Second Example Ltd", "email": "hello@example.co.uk", "companyDomain": "example.co.uk" } ] } }' ``` Up to 500 per call. You get back one `{ leadId, duplicate }` per row, in order. --- ## Something happened: a form, a trial, a download `leads.add` says *this person exists*. `events.ingest` says *this happened*, which is usually what you actually have. One call, and Selda works out whether this is somebody new, somebody it already knows, or an address that asked to be left alone. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fn": "events.ingest", "args": { "projectId": "'"$SELDA_PROJECT_ID"'", "type": "form_submitted", "identity": { "email": "matti@example.fi", "name": "Matti Meikalainen", "company": "Example Oy" }, "payload": { "form": "contact", "message": "What does it cost?" }, "source": "example.com", "idempotencyKey": "contact-matti@example.fi-2026-08-21", "autoAdvance": true } }' ``` **`idempotencyKey` is what makes a retry safe.** Derive it from the submission, never from a random value: the same key replays instead of creating a second lead for the same person. **`autoAdvance: true`** makes Selda read your Brain and write the reply, in the language the enquiry was written in, and leave it in the Sales Inbox. It spends credits and **it never sends**. Leave it out and the lead is simply stored. If the workspace has no Brain material to answer from, nothing is drafted and you get `no_brain_material`. That is deliberate: a quote answered from an empty Brain is an invented price. Full argument list: [inbound reference](/reference/mcp#inbound-telling-selda-something-happened). --- ## Read your pipeline ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"fn":"campaigns.stats","args":{"campaignId":"..."}}' ``` ```json { "value": { "sent": 42, "replies": 7, "meetings": 2 } } ``` Other reads you will want: `leads.list` (paginated, filter by tag and status), `leads.stats`, `campaigns.statsDetailed` (daily breakdown), `messages.list`, `replies.list`, `credits.info`. [All of them](/connect-your-app/reference#read-post-mcpquery). --- ## Run a campaign from a brief ```bash # 1. Create the campaign curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" -H "Content-Type: application/json" \ -d '{"fn":"campaigns.create","args":{"projectId":"'"$SELDA_PROJECT_ID"'","name":"Q3 push","leadCount":30}}' # 2. Start the engine curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer $SELDA_API_KEY" -H "Content-Type: application/json" \ -d '{"fn":"engine.start","args":{"projectId":"'"$SELDA_PROJECT_ID"'"}}' # 3. Poll, and read awaitingHuman before you say it finished curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer $SELDA_API_KEY" -H "Content-Type: application/json" \ -d '{"fn":"runs.status","args":{"runId":"..."}}' ``` **The engine stops at the company list and waits for a person** to confirm it in the app. That is not a timeout, it is the design. Running it costs credits, so do not put it in a loop. --- ## One complete flow A restaurant signs up on your site, Selda reaches out, the reply comes back to your backend. ``` 1. New restaurant on your site POST /mcp/mutate fn=leads.add { company: "Example Restaurant", email: "owner@example.fi", notes: "views:1200 · no_video:true" } 2. Selda finds the contact, researches, writes a personalised message 3. A human approves it in the Selda app, and Selda sends 4. Jani replies, and Selda POSTs your endpoint: { "event": "reply.received", "data": { "company": "Example Restaurant", "email": "owner@example.fi", "classification": "positive", "campaignId": "..." } } 5. Your backend marks the restaurant as a hot lead ``` Mapping your fields onto Selda's is usually the only design work: | Your field | Selda field | | --- | --- | | `restaurantName` | `company` | | `website` | `companyDomain` | | `ownerEmail` | `email` | | `ownerFirstName` | `firstName` | | `ownerLastName` | `lastName` | | `city` | `notes` | Step 4 needs a webhook: [how to register and verify one](/connect-your-app/webhooks). --- ## Teach Selda about your business The engine writes better when it knows what you sell. This is the same Brain the app edits. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fn": "knowledge.append", "args": { "projectId": "'"$SELDA_PROJECT_ID"'", "text": "We sell media packages: video production 990 EUR, distribution to 14k daily readers. Target: terrace restaurants without a video yet." } }' ``` `knowledge.get` reads it, `knowledge.set` replaces the whole thing, `knowledge.append` adds to it. To steer a campaign that is already running, `campaign.updatePlaybook` with `appendText`. --- ## When something goes wrong | Status | What it means | What to do | | --- | --- | --- | | `401` | Missing or revoked key | Check the header, make a new key | | `403` | The key lacks the scope | `read` for query, `write` for mutate, `pipeline` for run | | `400` | Unknown `fn` | Check the spelling against the [reference](/connect-your-app/reference) | | `429` | Too many calls | Wait and retry. The limit is per key | | `500` | The function threw | The message says what, usually a bad id | A refusal always carries a reason. `blocked: true` on a lead and a `429` are different problems and only one of them is worth retrying. --- # Webhooks Polling for replies works and wastes both our time. Register a URL instead and Selda POSTs you a signed payload whenever something happens. ## Register one In the app: **Settings → Apps → Outbound webhooks → Add**. Or: ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fn": "webhooks.create", "args": { "url": "https://example.com/selda/events", "events": ["reply.received", "lead.status_changed"], "description": "My app" } }' ``` ```json { "value": { "webhookId": "...", "secret": "whsec_..." } } ``` **The secret is shown once.** It is what proves a delivery came from Selda; store it where your receiver can read it. Subscribe with `"events": ["*"]` to receive everything. ## Events | Event | When | | --- | --- | | `lead.added` | A lead is created via the API (`leads.add` / `leads.addBatch`) | | `reply.received` | An inbound reply arrives in the Sales Inbox | | `message.sent` | A message is sent to a lead (campaign sends and API sends) | | `lead.status_changed` | Status changes to `responded` / `qualified`, or the CRM stage moves (`contacted` → `replied` → `meeting` → `customer` / `lost`) | | `campaign.completed` | A campaign run finishes sending | | `credits.low` | The balance drops below what one run costs. Fires once per crossing, before a campaign runs out mid-send. Read the exact number from `credits.info`, it is derived from measured cost and is not a fixed 20 | | `meeting.booked` | A prospect books a meeting through Selda | | `draft.ready` | Selda finished writing a draft, for example after `events.ingest` with `autoAdvance` | | `test.ping` | Sent by the "Send test" button, verify your endpoint with it | ## What arrives Every delivery is `POST { event, timestamp, data }` with two headers: `X-Selda-Event` and `X-Selda-Signature`. ```json { "event": "reply.received", "timestamp": 1720000000000, "data": { "inboundMessageId": "...", "leadId": "...", "email": "owner@example.fi", "company": "Example Oy", "campaignId": "...", "from": "owner@example.fi", "subject": "Re: Quick question", "classification": "positive" } } ``` ## Verify the signature Do this before trusting anything in the body. The signature is an HMAC-SHA256 of the **raw** bytes, so verify before parsing, not after. ```js const crypto = require("crypto"); function verifySeldaWebhook(secret, rawBody, signatureHeader) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected)); } // Express. Note express.raw, not express.json: re-serialising the body changes the bytes. app.post("/selda/events", express.raw({ type: "application/json" }), (req, res) => { if ( !verifySeldaWebhook( process.env.SELDA_WEBHOOK_SECRET, req.body, req.headers["x-selda-signature"], ) ) { return res.status(401).send("Bad signature"); } const { event, data } = JSON.parse(req.body); // handle... res.sendStatus(200); }); ``` ## Delivery, retries and being switched off - **10 second timeout** per request. - On a non-2xx response or a network error, Selda retries up to **2 more times**: 30 seconds, then 5 minutes after the first attempt. Retries carry a byte-identical body and signature, so your verification and your idempotency both still work. - After **10 consecutive deliveries where every attempt failed**, the endpoint is auto-disabled (`active: false`, `disabledAt` set). You can see that in `webhooks.list` and in **Settings → Apps → Outbound webhooks**, and re-enable it once your receiver is fixed. Nothing is silently dropped: a disabled endpoint is a state you can read, not an absence you have to notice. ## No webhook receiver? Use Zapier, Make or n8n Point the webhook at a "Catch Hook" trigger and do the rest there. The same works in reverse: those tools can `POST` to `https://api.selda.ai/mcp/mutate` with an `Authorization: Bearer sk_…` header, so a Selda integration is reachable without writing a backend at all. ## Next - [Examples](/connect-your-app/examples) - [Function reference](/connect-your-app/reference) - [Security and privacy](/security), how keys and webhook secrets are stored --- {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # Function reference Three endpoints, one body shape. The unit on this page is the **function name**: what you choose is the `fn` you put in the body, and the badge tells you which endpoint it goes to. ``` POST https://api.selda.ai/mcp/query read something needs the read scope POST https://api.selda.ai/mcp/mutate change something needs the write scope POST https://api.selda.ai/mcp/run start slow work needs the pipeline scope Authorization: Bearer sk_live_... { "fn": "", "args": { ... } } ``` Most of these also have a REST path: `GET /v1/leads/{leadId}`, `POST /v1/runs/{runId}/confirm-companies`. They are the same call at a different URL, answered by the same dispatcher, so a path can reach nothing this form refuses. The list is at [REST paths](/reference/rest) and in [openapi.json](https://docs.selda.ai/openapi.json). This page and every function page under **Reference / API functions** are generated from `convex/lib/mcpRegistry.ts`, the same table the endpoints dispatch from. A function added to the registry appears here on the next build; one removed disappears. A test fails if the two ever disagree, which is why the tables below no longer list functions that do not exist. ## Authentication ``` Authorization: Bearer sk_live_... ``` **Your organisation is resolved from the key.** You never send an `orgId`, and a key cannot reach a workspace outside the organisation it was made in. Keys are stored hashed, shown once, and revocable from **Settings**. `sk_test_...` keys reach every read and write that costs Selda nothing and **cannot spend or send**. The table below marks the ones a test key is refused, and says why. ## The response envelope ```json { "value": ..., "request_id": "req_..." } ``` On failure: ```json { "error": { "type": "...", "code": "...", "message": "...", "request_id": "req_..." }, "request_id": "req_..." } ``` `request_id` is also in the `X-Request-Id` header, so a caller can correlate a request without parsing the body. It is safe to log and to quote in a support ticket. | Status | Meaning | | --- | --- | | `401` | Missing, expired or revoked key | | `403` | The key lacks the scope, the plan entitlement, or is a sandbox key on a live-only function | | `400` | Unknown `fn`, or arguments the validator refused | | `429` | Rate limited. Wait and retry | | `500` | The function threw. The message says what, without leaking internals | ## Every function 74 registry entries across 72 names. 74 have their arguments derived from the target function's own validator; 0 are partly derived; 0 say so instead of guessing. | | `fn` | Scope | What it does | Limits | | --- | --- | --- | --- | --- | | `mutate` | [`brain.add`](/reference/api/brain/add) | `write` | Add one thing Selda should know: a product, a partner, a reference, a company fact, a note, something it must never say, or a `writing_rule` — a standing instruction about HOW messages are written, which reaches the composer as a directive and is never quoted as material. | | | `query` | [`brain.list`](/reference/api/brain/list) | `read` | The workspace's structured knowledge: products, partners, references, company facts, and the things Selda must never say. Each item has a type, a title and a body. | | | `mutate` | [`brain.remove`](/reference/api/brain/remove) | `write` | Take one Brain item back out. The human owns what Selda knows. | | | `mutate` | [`brain.update`](/reference/api/brain/update) | `write` | Rewrite the title and body of one Brain item. | | | `mutate` | [`campaigns.addLeads`](/reference/api/campaigns/add-leads) | `write` | Put specific leads into a campaign. | | | `mutate` | [`campaigns.addLeadsByTag`](/reference/api/campaigns/add-leads-by-tag) | `write` | Put every lead carrying a tag into a campaign (legacy table). | | | `mutate` | [`campaigns.addRule`](/reference/api/campaigns/add-rule) | `write` | Add a campaign rule. | | | `mutate` | [`campaigns.create`](/reference/api/campaigns/create) | `write` | Create a campaign (legacy table, not the one the app's campaign-flow UI reads). | | | `query` | [`campaigns.get`](/reference/api/campaigns/get) | `read` | One campaign: status, channels, settings, leads. | | | `query` | [`campaigns.list`](/reference/api/campaigns/list) | `read` | Campaigns in a workspace. | | | `mutate` | [`campaigns.lockMessageStructure`](/reference/api/campaigns/lock-message-structure) | `write` | Lock a campaign's message structure so every locked block ships exactly as written and nothing rewrites it, or unlock it with locked: false. Sends nothing. | | | `query` | [`campaigns.messageStructure`](/reference/api/campaigns/message-structure) | `read` | Read what a campaign's message is made of: every block, which ones ship verbatim, the instruction behind each generated one, the shape, and whether it is locked. | | | `mutate` | [`campaigns.setMessageStructure`](/reference/api/campaigns/set-message-structure) | `write` | State what a campaign's message is made of: blocks that ship WORD FOR WORD, blocks Selda writes from an instruction you give it, the paragraph count, and what must never appear. Refuses to change a locked structure. Sends nothing. | | | `query` | [`campaigns.stats`](/reference/api/campaigns/stats) | `read` | Campaign counters: sent, delivered, opened, clicked, replied, bounced. | | | `mutate` | [`campaigns.update`](/reference/api/campaigns/update) | `write` | Change a campaign. | | | `run` | [`company.lookup`](/reference/api/company/lookup) | `pipeline` | Resolve a company and return the right people to reach. Starts nothing. | live key only | | `mutate` | [`connectors.create`](/reference/api/connectors/create) | `write` | Register a data connector. | | | `mutate` | [`connectors.delete`](/reference/api/connectors/delete) | `write` | Remove a data connector. | | | `query` | [`connectors.list`](/reference/api/connectors/list) | `read` | Data connectors registered for this workspace. | | | `run` | [`connectors.sync`](/reference/api/connectors/sync) | `pipeline` | Pull from a connected data source. | live key only | | `query` | [`credits.info`](/reference/api/credits/info) | `read` | Credit balance, daily free credits, usage, plan. | | | `mutate` | [`drafts.remove`](/reference/api/drafts/remove) | `write` | Take one draft out of a run so it cannot be sent. The row stays visible with your reason and the app can put it back. Refuses a message that already went out. | | | `mutate` | [`drafts.update`](/reference/api/drafts/update) | `write` | Rewrite the draft on one run lead. Refuses a message that already went out; never sends. | | | `run` | [`engine.start`](/reference/api/engine/start) | `pipeline` | The full pipeline from a brief: find companies → research → fit → hook → draft. It STOPS at the company list (run status `awaiting_profile`) and waits for a person to confirm the companies and the decision-maker roles in the Selda app. Poll `runs.status` and read `awaitingHuman`. Nothing is ever sent from here. | live key only | | `mutate` | [`events.ingest`](/reference/api/events/ingest) | `write` | Report that something happened outside Selda (a form, an analysis, an ad response). Creates the lead if new, recognises it if known, records it on the timeline, and can put it on a campaign's review list. Pass autoAdvance to have Selda write the reply from the Brain straight away and leave it in the Sales Inbox, draft.ready is published when it is there. It never sends. | needs `inboundIntake` | | `mutate` | [`flows.create`](/reference/api/flows/create) | `write` | Create a flow: a trigger plus the steps to run when something arrives. Off unless you say otherwise. No step can send. | | | `mutate` | [`flows.delete`](/reference/api/flows/delete) | `write` | Delete a flow and its run log. | | | `query` | [`flows.list`](/reference/api/flows/list) | `read` | The flows in a workspace: what runs when something arrives from outside, the steps in order, and whether each is switched on. Includes the workspace's flow instruction files. | | | `query` | [`flows.runs`](/reference/api/flows/runs) | `read` | What a flow actually did, run by run, step by step, including the steps that did nothing and why. | | | `mutate` | [`flows.saveSkill`](/reference/api/flows/save-skill) | `write` | Write or rewrite an instruction file a flow step reads: how this business decides what an enquiry is. | | | `mutate` | [`flows.setEnabled`](/reference/api/flows/set-enabled) | `write` | Switch a flow on or off. | | | `mutate` | [`flows.update`](/reference/api/flows/update) | `write` | Rewrite a flow's name, trigger or steps. | | | `mutate` | [`inbox.addMessage`](/reference/api/inbox/add-message) | `write` | Put one message into a lead's Sales Inbox thread, in either direction, even for somebody who was never in a campaign. Creates the lead if it is new. This can send nothing. | | | `mutate` | [`knowledge.append`](/reference/api/knowledge/append) | `write` | Add to what Selda knows about your business. | | | `query` | [`knowledge.get`](/reference/api/knowledge/get) | `read` | What Selda knows about your business: the prose that grounds every message. | | | `mutate` | [`knowledge.set`](/reference/api/knowledge/set) | `write` | Replace what Selda knows about your business. | | | `mutate` | [`leads.add`](/reference/api/leads/add) | `write` | Add one company/contact. Pass `analysis` with research you already did and the message is written from it instead of a fresh crawl. | | | `mutate` | [`leads.addAlias`](/reference/api/leads/add-alias) | `write` | Claim another email address for a lead, so a reply from it lands in the same conversation. Also adopts that address's earlier unlinked inbound. | | | `mutate` | [`leads.addBatch`](/reference/api/leads/add-batch) | `write` | Add many companies/contacts in one call. | | | `mutate` | [`leads.addTag`](/reference/api/leads/add-tag) | `write` | Tag a lead. | | | `mutate` | [`leads.delete`](/reference/api/leads/delete) | `write` | Remove one lead. Deleting is the caller's act, Selda never removes a lead on its own. | | | `mutate` | [`leads.deleteBatch`](/reference/api/leads/delete-batch) | `write` | Remove many leads. | | | `run` | [`leads.enrich`](/reference/api/leads/enrich) | `pipeline` | Enrich one lead from a natural-language instruction. | live key only | | `run` | [`leads.enrich`](/reference/api/leads/enrich) | `pipeline` | Enrich a workspace's leads from a natural-language instruction (legacy path). | live key only, **not reachable** | | `run` | [`leads.enrichBatch`](/reference/api/leads/enrich-batch) | `pipeline` | Enrich many leads from a natural-language instruction. | live key only | | `query` | [`leads.get`](/reference/api/leads/get) | `read` | One lead in full: research, fit, outreach angle, notes. | | | `query` | [`leads.list`](/reference/api/leads/list) | `read` | Leads in a workspace. | | | `mutate` | [`leads.merge`](/reference/api/leads/merge) | `write` | Merge duplicate leads. | | | `mutate` | [`leads.skip`](/reference/api/leads/skip) | `write` | DELETES a lead and every message on it (legacy path, Clerk-authenticated, an API key cannot reach this; use leads.delete instead). | | | `mutate` | [`leads.update`](/reference/api/leads/update) | `write` | Edit a lead's fields, including its status. Org-scoped, so an API key can reach it. | | | `mutate` | [`leads.update`](/reference/api/leads/update) | `write` | Edit a lead (legacy path, shadowed by the org-scoped leads.update above). | **not reachable** | | `mutate` | [`leads.updateStatus`](/reference/api/leads/update-status) | `write` | Set a lead's status (legacy path, Clerk-authenticated, an API key cannot reach this; the MCP tool uses the org-scoped leads.update). | | | `run` | [`material.import`](/reference/api/material/import) | `pipeline` | Your prospect folder → a campaign + company list, then it stops. Uploading material is not permission to send. | live key for some arguments | | `upload` | [`POST /mcp/material/upload`](/reference/api/material/upload) | `pipeline` | Raw file bytes in, storageId out. Send the file's path in X-Selda-Path. That path is how Selda maps a file to a company. Then hand the ids to material.import. | | | `mutate` | [`messages.approve`](/reference/api/messages/approve) | `write` | Approve a drafted message. Approval only. It does not send. | | | `query` | [`messages.byLead`](/reference/api/messages/by-lead) | `read` | The whole thread with one lead, sent and received. | | | `query` | [`messages.byProject`](/reference/api/messages/by-project) | `read` | Messages in a workspace. | | | `run` | [`messages.generate`](/reference/api/messages/generate) | `pipeline` | Draft a message for a lead. | | | `query` | [`projects.get`](/reference/api/projects/get) | `read` | One workspace in full: business context, market analysis, ICP, settings. | | | `query` | [`projects.list`](/reference/api/projects/list) | `read` | Your workspaces. Start here. Every other fn needs a projectId. | | | `mutate` | [`projects.updateContext`](/reference/api/projects/update-context) | `write` | Rewrite a workspace's business context. | | | `run` | [`replies.classify`](/reference/api/replies/classify) | `pipeline` | Classify inbound replies. | | | `mutate` | [`replies.draft`](/reference/api/replies/draft) | `write` | Write a reply draft into a lead's Sales Inbox thread. A person reviews and sends it in the app, this can send nothing. | | | `run` | [`replies.draft`](/reference/api/replies/draft) | `pipeline` | Draft an answer to a reply. | | | `run` | [`replies.preview`](/reference/api/replies/preview) | `pipeline` | Ask how Selda would answer an enquiry, from this workspace's Brain, without creating a lead or storing a draft. Same writer the real reply uses, so tuning against this tunes the real thing. Stores nothing and sends nothing. | | | `mutate` | [`runs.archive`](/reference/api/runs/archive) | `write` | Close a campaign run and take it off the active list. Keeps every contact and every message, deleting contacts stays a human act in the app. | | | `mutate` | [`runs.confirmCompanies`](/reference/api/runs/confirm-companies) | `write` | Confirm a run's company list so Selda finds the decision-makers and drafts the messages. Spends credits. Sends nothing, the send is still a human press in the app. | live key only | | `query` | [`runs.leads`](/reference/api/runs/leads) | `read` | The companies a run found, each with the message Selda drafted for it. Nothing is sent. | | | `query` | [`runs.list`](/reference/api/runs/list) | `read` | Every campaign run in a project, newest first, with its status. Use it to find a runId you no longer have. Runs the human archived are left out; pass includeArchived: true to see them too. | | | `mutate` | [`runs.rename`](/reference/api/runs/rename) | `write` | Give a campaign run a name a person would recognise. An empty name restores the derived title. | | | `run` | [`runs.startFromLeads`](/reference/api/runs/start-from-leads) | `pipeline` | Start a campaign from leads already pushed in with selda_add_lead, selected by the source label you gave them. No discovery, Selda writes a message per lead from the analysis that came with it, and stops at the drafts. | live key only | | `query` | [`runs.status`](/reference/api/runs/status) | `read` | Status of one campaign run: phase, companies found, contacts resolved, drafts written, errors. | | | `mutate` | [`webhooks.create`](/reference/api/webhooks/create) | `write` | Register an endpoint for events like reply.received. | | | `mutate` | [`webhooks.delete`](/reference/api/webhooks/delete) | `write` | Remove a webhook endpoint. | | | `query` | [`webhooks.list`](/reference/api/webhooks/list) | `read` | Outbound webhook endpoints registered for this workspace. | | ## What is not here, on purpose **There is no function that sends.** `launchRun` is in no registry, not this one and not the MCP one, and it never will be. A script can prepare a campaign completely: import material, confirm the companies, write and rewrite every draft. A human presses send in the app. That is the product's central claim, not a gap waiting to be filled. **There is no function that creates a workspace.** `projects.create` is absent rather than gated, because a rule enforced by a plan tier is a rule with a price on it. --- The live manifest, always current and needing no key: `GET https://api.selda.ai/mcp/capabilities`. --- {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} # API functions One page per function name, generated from `convex/lib/mcpRegistry.ts`. Each page states the endpoint it goes to, the scope it needs, whether a free sandbox key can call it, what it takes and an example request. The badge on each sidebar entry is the endpoint, not an HTTP verb: `query` is `POST /mcp/query`, `mutate` is `POST /mcp/mutate`, `run` is `POST /mcp/run`, `upload` is `POST /mcp/material/upload`. For authentication, the response envelope and the error codes, read [the function reference](/connect-your-app/reference). | Group | Count | Functions | | --- | --- | --- | | `brain` | 4 | [`brain.add`](/reference/api/brain/add), [`brain.list`](/reference/api/brain/list), [`brain.remove`](/reference/api/brain/remove), [`brain.update`](/reference/api/brain/update) | | `campaigns` | 11 | [`campaigns.addLeads`](/reference/api/campaigns/add-leads), [`campaigns.addLeadsByTag`](/reference/api/campaigns/add-leads-by-tag), [`campaigns.addRule`](/reference/api/campaigns/add-rule), [`campaigns.create`](/reference/api/campaigns/create), [`campaigns.get`](/reference/api/campaigns/get), [`campaigns.list`](/reference/api/campaigns/list), [`campaigns.lockMessageStructure`](/reference/api/campaigns/lock-message-structure), [`campaigns.messageStructure`](/reference/api/campaigns/message-structure), [`campaigns.setMessageStructure`](/reference/api/campaigns/set-message-structure), [`campaigns.stats`](/reference/api/campaigns/stats), [`campaigns.update`](/reference/api/campaigns/update) | | `company` | 1 | [`company.lookup`](/reference/api/company/lookup) | | `connectors` | 4 | [`connectors.create`](/reference/api/connectors/create), [`connectors.delete`](/reference/api/connectors/delete), [`connectors.list`](/reference/api/connectors/list), [`connectors.sync`](/reference/api/connectors/sync) | | `credits` | 1 | [`credits.info`](/reference/api/credits/info) | | `drafts` | 2 | [`drafts.remove`](/reference/api/drafts/remove), [`drafts.update`](/reference/api/drafts/update) | | `engine` | 1 | [`engine.start`](/reference/api/engine/start) | | `events` | 1 | [`events.ingest`](/reference/api/events/ingest) | | `flows` | 7 | [`flows.create`](/reference/api/flows/create), [`flows.delete`](/reference/api/flows/delete), [`flows.list`](/reference/api/flows/list), [`flows.runs`](/reference/api/flows/runs), [`flows.saveSkill`](/reference/api/flows/save-skill), [`flows.setEnabled`](/reference/api/flows/set-enabled), [`flows.update`](/reference/api/flows/update) | | `inbox` | 1 | [`inbox.addMessage`](/reference/api/inbox/add-message) | | `knowledge` | 3 | [`knowledge.append`](/reference/api/knowledge/append), [`knowledge.get`](/reference/api/knowledge/get), [`knowledge.set`](/reference/api/knowledge/set) | | `leads` | 14 | [`leads.add`](/reference/api/leads/add), [`leads.addAlias`](/reference/api/leads/add-alias), [`leads.addBatch`](/reference/api/leads/add-batch), [`leads.addTag`](/reference/api/leads/add-tag), [`leads.delete`](/reference/api/leads/delete), [`leads.deleteBatch`](/reference/api/leads/delete-batch), [`leads.enrich`](/reference/api/leads/enrich), [`leads.enrichBatch`](/reference/api/leads/enrich-batch), [`leads.get`](/reference/api/leads/get), [`leads.list`](/reference/api/leads/list), [`leads.merge`](/reference/api/leads/merge), [`leads.skip`](/reference/api/leads/skip), [`leads.update`](/reference/api/leads/update), [`leads.updateStatus`](/reference/api/leads/update-status) | | `material` | 2 | [`material.import`](/reference/api/material/import), [`POST /mcp/material/upload`](/reference/api/material/upload) | | `messages` | 4 | [`messages.approve`](/reference/api/messages/approve), [`messages.byLead`](/reference/api/messages/by-lead), [`messages.byProject`](/reference/api/messages/by-project), [`messages.generate`](/reference/api/messages/generate) | | `projects` | 3 | [`projects.get`](/reference/api/projects/get), [`projects.list`](/reference/api/projects/list), [`projects.updateContext`](/reference/api/projects/update-context) | | `replies` | 3 | [`replies.classify`](/reference/api/replies/classify), [`replies.draft`](/reference/api/replies/draft), [`replies.preview`](/reference/api/replies/preview) | | `runs` | 7 | [`runs.archive`](/reference/api/runs/archive), [`runs.confirmCompanies`](/reference/api/runs/confirm-companies), [`runs.leads`](/reference/api/runs/leads), [`runs.list`](/reference/api/runs/list), [`runs.rename`](/reference/api/runs/rename), [`runs.startFromLeads`](/reference/api/runs/start-from-leads), [`runs.status`](/reference/api/runs/status) | | `webhooks` | 3 | [`webhooks.create`](/reference/api/webhooks/create), [`webhooks.delete`](/reference/api/webhooks/delete), [`webhooks.list`](/reference/api/webhooks/list) | --- {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `brain.add` > Add one thing Selda should know: a product, a partner, a reference, a company fact, a note, something it must never say, or a `writing_rule` — a standing instruction about HOW messages are written, which reaches the composer as a directive and is never quoted as material. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_add_brain_item` | | Convex function | `mcpQueries:addBrainItemInternal` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `type` | `"company" \| "product" \| "partner" \| "reference" \| "avoid" \| ...` | **required** | | | `title` | `string` | **required** | | | `body` | `string` | **required** | | Shapes that did not fit the table: ```ts type: "company" | "product" | "partner" | "reference" | "avoid" | "note" | "writing_rule" ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "brain.add", "args": { "projectId": "", "type": "company", "title": "", "body": "<body>" } }' ``` ```json { "fn": "brain.add", "args": { "projectId": "<projectId>", "type": "company", "title": "<title>", "body": "<body>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addBrainItemInternal` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/brain/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `brain.list` > The workspace's structured knowledge: products, partners, references, company facts, and the things Selda must never say. Each item has a type, a title and a body. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_brain` | | Convex function | `mcpQueries:listBrainItems` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "brain.list", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "brain.list", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:listBrainItems` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/brain/remove --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `brain.remove` > Take one Brain item back out. The human owns what Selda knows. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_remove_brain_item` | | Convex function | `mcpQueries:removeBrainItemInternal` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `id` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "brain.remove", "args": { "projectId": "<projectId>", "id": "<id>" } }' ``` ```json { "fn": "brain.remove", "args": { "projectId": "<projectId>", "id": "<id>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:removeBrainItemInternal` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/brain/update --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `brain.update` > Rewrite the title and body of one Brain item. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_update_brain_item` | | Convex function | `mcpQueries:updateBrainItemInternal` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `id` | `string` | **required** | | | `title` | `string` | optional | | | `body` | `string` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "brain.update", "args": { "projectId": "<projectId>", "id": "<id>" } }' ``` ```json { "fn": "brain.update", "args": { "projectId": "<projectId>", "id": "<id>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:updateBrainItemInternal` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/add-leads-by-tag --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.addLeadsByTag` > Put every lead carrying a tag into a campaign (legacy table). | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `mcpQueries:addLeadsToCampaignByTag` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | | `projectId` | `string` | **required** | | | `tags` | `string[]` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.addLeadsByTag", "args": { "campaignId": "<campaignId>", "projectId": "<projectId>", "tags": ["<tags>"] } }' ``` ```json { "fn": "campaigns.addLeadsByTag", "args": { "campaignId": "<campaignId>", "projectId": "<projectId>", "tags": ["<tags>"] } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addLeadsToCampaignByTag` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/add-leads --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.addLeads` > Put specific leads into a campaign. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `mcpQueries:addLeadsToCampaign` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | | `leadIds` | `id<"leads">[]` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.addLeads", "args": { "campaignId": "<campaignId>", "leadIds": "<leadIds>" } }' ``` ```json { "fn": "campaigns.addLeads", "args": { "campaignId": "<campaignId>", "leadIds": "<leadIds>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addLeadsToCampaign` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/add-rule --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.addRule` > Add a campaign rule. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `mcpQueries:addCampaignRule` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `campaignId` | `id<"campaigns">` | **required** | | | `condition` | `{ tag: string }` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.addRule", "args": { "projectId": "<projectId>", "campaignId": "<campaignId>", "condition": {} } }' ``` ```json { "fn": "campaigns.addRule", "args": { "projectId": "<projectId>", "campaignId": "<campaignId>", "condition": {} } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addCampaignRule` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/create --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.create` > Create a campaign (legacy table, not the one the app's campaign-flow UI reads). | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `mcpQueries:createCampaign` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `name` | `string` | **required** | | | `brief` | `string` | optional | | | `outreachAngle` | `string` | optional | | | `channels` | `string[]` | optional | | | `targetRole` | `string` | optional | | | `targetIndustry` | `string` | optional | | | `targetCompanySize` | `string` | optional | | | `locations` | `string[]` | optional | | | `leadCount` | `number` | optional | | | `startAt` | `number \| null` | optional | | | `startedAt` | `number \| null` | optional | | | `scheduledStartDate` | `number` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | | `userId` | the user the API key belongs to | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.create", "args": { "projectId": "<projectId>", "name": "<name>" } }' ``` ```json { "fn": "campaigns.create", "args": { "projectId": "<projectId>", "name": "<name>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:createCampaign` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/get --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.get` > One campaign: status, channels, settings, leads. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_campaign` | | Convex function | `mcpQueries:getCampaign` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.get", "args": { "campaignId": "<campaignId>" } }' ``` ```json { "fn": "campaigns.get", "args": { "campaignId": "<campaignId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getCampaign` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.list` > Campaigns in a workspace. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_campaigns` | | Convex function | `mcpQueries:listCampaigns` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.list", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "campaigns.list", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:listCampaigns` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/lock-message-structure --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.lockMessageStructure` > Lock a campaign's message structure so every locked block ships exactly as written and nothing rewrites it, or unlock it with locked: false. Sends nothing. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_lock_message_structure` | | Convex function | `campaignBlueprints:setMessageStructureLockForApi` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | | `locked` | `boolean` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.lockMessageStructure", "args": { "campaignId": "<campaignId>", "locked": true } }' ``` ```json { "fn": "campaigns.lockMessageStructure", "args": { "campaignId": "<campaignId>", "locked": true } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignBlueprints:setMessageStructureLockForApi` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/message-structure --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.messageStructure` > Read what a campaign's message is made of: every block, which ones ship verbatim, the instruction behind each generated one, the shape, and whether it is locked. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_message_structure` | | Convex function | `campaignBlueprints:messageStructureForApi` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.messageStructure", "args": { "campaignId": "<campaignId>" } }' ``` ```json { "fn": "campaigns.messageStructure", "args": { "campaignId": "<campaignId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <any | null>, "request_id": "req_..." } ``` The function declares a return validator, so `value` is `any | null`. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/set-message-structure --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.setMessageStructure` > State what a campaign's message is made of: blocks that ship WORD FOR WORD, blocks Selda writes from an instruction you give it, the paragraph count, and what must never appear. Refuses to change a locked structure. Sends nothing. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_set_message_structure` | | Convex function | `campaignBlueprints:setMessageStructureForApi` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | | `objective` | `string` | optional | | | `blocks` | `object[]` | optional | | | `form` | `object` | optional | | | `mustNever` | `string[]` | optional | | | `must` | `string[]` | optional | | | `followUpStrategy` | `string` | optional | | Shapes that did not fit the table: ```ts blocks: ({ id?: string; order?: number; role?: "subject" | "body"; kind: "locked" | "generated"; label?: string; content?: string; aiInstruction?: string; optional?: boolean })[] form: { paragraphs?: number; maxWords?: number; links?: "none" | "allowed" } ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.setMessageStructure", "args": { "campaignId": "<campaignId>" } }' ``` ```json { "fn": "campaigns.setMessageStructure", "args": { "campaignId": "<campaignId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignBlueprints:setMessageStructureForApi` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/stats --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.stats` > Campaign counters: sent, delivered, opened, clicked, replied, bounced. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_campaign_stats` | | Convex function | `mcpQueries:getCampaignStats` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.stats", "args": { "campaignId": "<campaignId>" } }' ``` ```json { "fn": "campaigns.stats", "args": { "campaignId": "<campaignId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getCampaignStats` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/campaigns/update --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `campaigns.update` > Change a campaign. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `mcpQueries:updateCampaign` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `campaignId` | `string` | **required** | | | `name` | `string` | optional | | | `goal` | `string` | optional | | | `brief` | `string \| null` | optional | | | `ghostwriterHints` | `string \| null` | optional | | | `outreachAngle` | `string` | optional | | | `scheduledStartDate` | `number \| null` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "campaigns.update", "args": { "campaignId": "<campaignId>" } }' ``` ```json { "fn": "campaigns.update", "args": { "campaignId": "<campaignId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:updateCampaign` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/company/lookup --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `company.lookup` > Resolve a company and return the right people to reach. Starts nothing. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | `selda_lookup` | | Convex function | `mcpQueries:lookupCompany` | A free sandbox key is refused: it resolves real people's contact details through a paid provider, which costs money per call. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `company` | `string` | **required** | | | `role` | `string` | optional | | | `limit` | `number` | optional | | | `domain` | `string` | optional | | | `city` | `string` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "company.lookup", "args": { "company": "<company>" } }' ``` ```json { "fn": "company.lookup", "args": { "company": "<company>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:lookupCompany` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/connectors/create --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `connectors.create` > Register a data connector. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `dataConnectors:createConnector` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `name` | `string` | **required** | | | `logoUrl` | `string` | optional | | | `url` | `string` | **required** | | | `authHeader` | `string` | optional | | | `fieldMap` | `object` | **required** | | Shapes that did not fit the table: ```ts fieldMap: { company?: string; email?: string; externalUrl?: string; companyDomain?: string; firstName?: string; lastName?: string; notes?: string } ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "connectors.create", "args": { "projectId": "<projectId>", "name": "<name>", "url": "<url>", "fieldMap": {} } }' ``` ```json { "fn": "connectors.create", "args": { "projectId": "<projectId>", "name": "<name>", "url": "<url>", "fieldMap": {} } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `dataConnectors:createConnector` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/connectors/delete --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `connectors.delete` > Remove a data connector. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `dataConnectors:deleteConnector` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `connectorId` | `id<"dataConnectors">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "connectors.delete", "args": { "connectorId": "<connectorId>" } }' ``` ```json { "fn": "connectors.delete", "args": { "connectorId": "<connectorId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `dataConnectors:deleteConnector` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/connectors/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `connectors.list` > Data connectors registered for this workspace. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_connectors` | | Convex function | `dataConnectors:listConnectors` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "connectors.list", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "connectors.list", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `dataConnectors:listConnectors` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/connectors/sync --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `connectors.sync` > Pull from a connected data source. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `dataConnectors:syncConnector` | A free sandbox key is refused: it pulls from a connected outside source and enriches as it goes. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `connectorId` | `id<"dataConnectors">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "connectors.sync", "args": { "connectorId": "<connectorId>" } }' ``` ```json { "fn": "connectors.sync", "args": { "connectorId": "<connectorId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `dataConnectors:syncConnector` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/credits/info --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `credits.info` > Credit balance, daily free credits, usage, plan. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_credits` | | Convex function | `mcpQueries:getCreditInfo` | A free sandbox key may call it. ## Arguments This function takes no arguments of its own. Send `"args": {}`. ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "credits.info", "args": {} }' ``` ```json { "fn": "credits.info", "args": {} } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getCreditInfo` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/drafts/remove --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `drafts.remove` > Take one draft out of a run so it cannot be sent. The row stays visible with your reason and the app can put it back. Refuses a message that already went out. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_remove_draft` | | Convex function | `campaignRunner/mutations:removeDraftFromApiKey` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `id<"campaignRunLeads">` | **required** | | | `reason` | `string` | optional | The caller's own words for why. Stored verbatim and shown on the row. | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "drafts.remove", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "drafts.remove", "args": { "leadId": "<leadId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignRunner/mutations:removeDraftFromApiKey` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/drafts/update --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `drafts.update` > Rewrite the draft on one run lead. Refuses a message that already went out; never sends. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_update_draft` | | Convex function | `mcpQueries:updateRunLeadDraft` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `string` | **required** | | | `subject` | `string` | optional | | | `body` | `string` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "drafts.update", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "drafts.update", "args": { "leadId": "<leadId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:updateRunLeadDraft` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/engine/start --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `engine.start` > The full pipeline from a brief: find companies → research → fit → hook → draft. It STOPS at the company list (run status `awaiting_profile`) and waits for a person to confirm the companies and the decision-maker roles in the Selda app. Poll `runs.status` and read `awaitingHuman`. Nothing is ever sent from here. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | `selda_run_pipeline` | | Convex function | `campaignRunner/index:startEngineFromApiKey` | A free sandbox key is refused: it runs the discovery and research engine: web search, crawls, model calls, per-lead credits. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `idea` | `string` | optional | | | `campaignId` | `id<"campaigns">` | optional | | | `targetLeadCount` | `number` | optional | | | `startAt` | `number` | optional | | | `channels` | `string[]` | optional | WHICH CHANNELS THIS CAMPAIGN SENDS ON, the API's version of the Määritä card's Kanavat step. Without it this door could not name a channel at all: it auto-creates a campaign, and `createCampaignForEngine` then stamps `["email"]`, so every LinkedIn campaign an agent or an integration started was an email campaign and nothing said so. It is read by discovery, by the contact ladder, by the writer and by `launchRun`, so a wrong value here is not cosmetic. Applies to the campaign THIS call creates. When `campaignId` names an existing campaign that campaign's own `channels` govern (change them with `campaigns.update`), the run must not be able to disagree with the campaign it belongs to. Absent = unchanged: the campaign's default. | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | | `apiKeyId` | the calling key, so the run records who started it | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "engine.start", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "engine.start", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignRunner/index:startEngineFromApiKey` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/events/ingest --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `events.ingest` > Report that something happened outside Selda (a form, an analysis, an ad response). Creates the lead if new, recognises it if known, records it on the timeline, and can put it on a campaign's review list. Pass autoAdvance to have Selda write the reply from the Brain straight away and leave it in the Sales Inbox, draft.ready is published when it is there. It never sends. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | `inboundIntake` | | MCP tools | `selda_ingest_event` | | Convex function | `mcpQueries:ingestEvent` | A free sandbox key may call it. <Callout type="warning">Needs the `inboundIntake` entitlement on top of the scope. Without it the call comes back `403` and says so, rather than doing half the work.</Callout> ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `type` | `string` | **required** | Caller-defined: "form_submitted", "analysis_completed", "contract_signed", anything. | | `identity` | `object` | **required** | | | `payload` | `any` | optional | Whatever the event carried. Stored whole; only summarized into the notes line. | | `source` | `string` | optional | Which system reported it, e.g. "evexcreative.com". Stored as `discoverySource`. | | `occurredAt` | `string \| number` | optional | ISO string or epoch ms. An unusable or out-of-range value falls back to now, and says so. | | `idempotencyKey` | `string` | optional | Replay protection, unique per org. A repeat returns the first result unchanged. | | `attachToRunId` | `id<"campaignRuns">` | optional | Put the lead on this campaign run's review list as well. The run's own tone, signature, follow-up rhythm and safety rules already exist and are already approved; an inbound lead joining them beats it inventing a process of its own. It joins at the review stage with no message written, exactly like every other lead on that run. | | `analysis` | `string` | optional | Research the caller already did, used by engineV3 as authoritative grounding. | | `tags` | `string[]` | optional | | | `autoAdvance` | `boolean` | optional | Write the reply now, from the Brain, and leave it in the Sales Inbox for a person. Off by default because it spends. It never sends: the draft waits for the same approval as every other draft, and `draft.ready` is published so nobody has to poll to find it. When drafting cannot be done the lead is untouched and `draft.failed` says why. | Shapes that did not fit the table: ```ts identity: { email?: string; domain?: string; linkedinUrl?: string; name?: string; firstName?: string; lastName?: string; company?: string; jobTitle?: string; phone?: string } ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "events.ingest", "args": { "projectId": "<projectId>", "type": "<type>", "identity": {} } }' ``` ```json { "fn": "events.ingest", "args": { "projectId": "<projectId>", "type": "<type>", "identity": {} } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:ingestEvent` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/flows/create --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `flows.create` > Create a flow: a trigger plus the steps to run when something arrives. Off unless you say otherwise. No step can send. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_create_flow` | | Convex function | `flows/mcp:createFlow` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `name` | `string` | **required** | | | `trigger` | `object` | **required** | | | `steps` | `object[]` | **required** | | | `enabled` | `boolean` | optional | Absent means off. A flow that starts answering the world the moment a script writes it is a decision the script made about somebody's website. | Shapes that did not fit the table: ```ts trigger: { kind: "inbound_event" | "manual"; eventTypes?: string[]; sources?: string[] } steps: ({ id: string; kind: "understand" | "route" | "attach" | "draft_reply" | "notify"; skillId?: id<"flowSkills">; categories?: object[]; routes?: object[]; runId?: id<"campaignRuns">; emails?: string[]; webhook?: boolean; note?: string })[] ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "flows.create", "args": { "projectId": "<projectId>", "name": "<name>", "trigger": {}, "steps": [{}] } }' ``` ```json { "fn": "flows.create", "args": { "projectId": "<projectId>", "name": "<name>", "trigger": {}, "steps": [{}] } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `flows/mcp:createFlow` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/flows/delete --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `flows.delete` > Delete a flow and its run log. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_delete_flow` | | Convex function | `flows/mcp:removeFlow` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `flowId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "flows.delete", "args": { "flowId": "<flowId>" } }' ``` ```json { "fn": "flows.delete", "args": { "flowId": "<flowId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `flows/mcp:removeFlow` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/flows/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `flows.list` > The flows in a workspace: what runs when something arrives from outside, the steps in order, and whether each is switched on. Includes the workspace's flow instruction files. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_flows` | | Convex function | `flows/mcp:listFlows` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "flows.list", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "flows.list", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `flows/mcp:listFlows` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/flows/runs --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `flows.runs` > What a flow actually did, run by run, step by step, including the steps that did nothing and why. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_flow_runs` | | Convex function | `flows/mcp:flowRuns` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `flowId` | `string` | **required** | | | `limit` | `number` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "flows.runs", "args": { "flowId": "<flowId>" } }' ``` ```json { "fn": "flows.runs", "args": { "flowId": "<flowId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `flows/mcp:flowRuns` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/flows/save-skill --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `flows.saveSkill` > Write or rewrite an instruction file a flow step reads: how this business decides what an enquiry is. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_save_flow_skill` | | Convex function | `flows/mcp:saveSkill` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `skillId` | `string` | optional | | | `name` | `string` | **required** | | | `body` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "flows.saveSkill", "args": { "projectId": "<projectId>", "name": "<name>", "body": "<body>" } }' ``` ```json { "fn": "flows.saveSkill", "args": { "projectId": "<projectId>", "name": "<name>", "body": "<body>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `flows/mcp:saveSkill` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/flows/set-enabled --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `flows.setEnabled` > Switch a flow on or off. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_set_flow_enabled` | | Convex function | `flows/mcp:setFlowEnabled` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `flowId` | `string` | **required** | | | `enabled` | `boolean` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "flows.setEnabled", "args": { "flowId": "<flowId>", "enabled": true } }' ``` ```json { "fn": "flows.setEnabled", "args": { "flowId": "<flowId>", "enabled": true } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `flows/mcp:setFlowEnabled` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/flows/update --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `flows.update` > Rewrite a flow's name, trigger or steps. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_update_flow` | | Convex function | `flows/mcp:updateFlow` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `flowId` | `string` | **required** | | | `name` | `string` | optional | | | `trigger` | `object` | optional | | | `steps` | `object[]` | optional | | Shapes that did not fit the table: ```ts trigger: { kind: "inbound_event" | "manual"; eventTypes?: string[]; sources?: string[] } ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "flows.update", "args": { "flowId": "<flowId>" } }' ``` ```json { "fn": "flows.update", "args": { "flowId": "<flowId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `flows/mcp:updateFlow` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/inbox/add-message --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `inbox.addMessage` > Put one message into a lead's Sales Inbox thread, in either direction, even for somebody who was never in a campaign. Creates the lead if it is new. This can send nothing. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_add_inbox_message` | | Convex function | `mcpQueries:addInboxMessage` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `leadId` | `string` | optional | An existing lead. Omit it and name the person in `identity` instead. | | `identity` | `object` | optional | | | `direction` | `"inbound" \| "outbound"` | **required** | `inbound` = they wrote it. `outbound` = we did, somewhere else. Nothing here sends. | | `body` | `string` | **required** | | | `subject` | `string` | optional | | | `occurredAt` | `string \| number` | optional | When it actually happened. Defaults to now; a wrong "now" on old history is a lie. | Shapes that did not fit the table: ```ts identity: { email?: string; name?: string; firstName?: string; lastName?: string; company?: string; jobTitle?: string; domain?: string; linkedinUrl?: string; phone?: string } ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "inbox.addMessage", "args": { "projectId": "<projectId>", "direction": "inbound", "body": "<body>" } }' ``` ```json { "fn": "inbox.addMessage", "args": { "projectId": "<projectId>", "direction": "inbound", "body": "<body>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addInboxMessage` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/knowledge/append --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `knowledge.append` > Add to what Selda knows about your business. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_append_knowledge` | | Convex function | `mcpQueries:appendKnowledge` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `text` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "knowledge.append", "args": { "projectId": "<projectId>", "text": "<text>" } }' ``` ```json { "fn": "knowledge.append", "args": { "projectId": "<projectId>", "text": "<text>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:appendKnowledge` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/knowledge/get --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `knowledge.get` > What Selda knows about your business: the prose that grounds every message. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_knowledge` | | Convex function | `mcpQueries:getKnowledge` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "knowledge.get", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "knowledge.get", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getKnowledge` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/knowledge/set --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `knowledge.set` > Replace what Selda knows about your business. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_set_knowledge` | | Convex function | `mcpQueries:setKnowledge` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `text` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "knowledge.set", "args": { "projectId": "<projectId>", "text": "<text>" } }' ``` ```json { "fn": "knowledge.set", "args": { "projectId": "<projectId>", "text": "<text>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:setKnowledge` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/add-alias --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.addAlias` > Claim another email address for a lead, so a reply from it lands in the same conversation. Also adopts that address's earlier unlinked inbound. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_add_lead_alias` | | Convex function | `mcpQueries:addLeadAlias` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `string` | **required** | | | `email` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.addAlias", "args": { "leadId": "<leadId>", "email": "<email>" } }' ``` ```json { "fn": "leads.addAlias", "args": { "leadId": "<leadId>", "email": "<email>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addLeadAlias` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/add-batch --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.addBatch` > Add many companies/contacts in one call. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_add_leads` | | Convex function | `mcpQueries:addLeadBatch` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `leads` | `object[]` | **required** | | Shapes that did not fit the table: ```ts leads: { firstName?: string; lastName?: string; email?: string; company?: string; companyDomain?: string; domain?: string; jobTitle?: string; linkedinUrl?: string; phone?: string; notes?: string; analysis?: string; externalUrl?: string; source?: string; tags?: string[]; city?: string; country?: string; location?: string; companyCity?: string; companyWebsite?: string; customFields?: any }[] ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.addBatch", "args": { "projectId": "<projectId>", "leads": [{}] } }' ``` ```json { "fn": "leads.addBatch", "args": { "projectId": "<projectId>", "leads": [{}] } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addLeadBatch` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/add-tag --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.addTag` > Tag a lead. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_tag_lead` | | Convex function | `mcpQueries:addLeadTag` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `id<"leads">` | **required** | | | `tags` | `string[]` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.addTag", "args": { "leadId": "<leadId>", "tags": ["<tags>"] } }' ``` ```json { "fn": "leads.addTag", "args": { "leadId": "<leadId>", "tags": ["<tags>"] } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addLeadTag` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/add --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.add` > Add one company/contact. Pass `analysis` with research you already did and the message is written from it instead of a fresh crawl. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_add_lead` | | Convex function | `mcpQueries:addLead` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `firstName` | `string` | optional | | | `lastName` | `string` | optional | | | `email` | `string` | optional | | | `company` | `string` | optional | | | `companyDomain` | `string` | optional | | | `jobTitle` | `string` | optional | | | `linkedinUrl` | `string` | optional | | | `phone` | `string` | optional | | | `notes` | `string` | optional | | | `analysis` | `string` | optional | | | `outreachAngle` | `string` | optional | The opening the operator already chose, and why they chose the company. Both are read by the engine (`operatorAngle` governs the opening, `whyGoodLead` feeds research and the fit call). They were wired into the engine on 17.8.2026 and had no door on this mutation for an hour, which is the same shape of gap the analysis had for a week: read by a writer, unreachable by the caller. | | `whyGoodLead` | `string` | optional | | | `externalUrl` | `string` | optional | | | `source` | `string` | optional | | | `tags` | `string[]` | optional | | | `city` | `string` | optional | | | `country` | `string` | optional | | | `location` | `string` | optional | | | `customFields` | `any` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.add", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "leads.add", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:addLead` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/delete-batch --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.deleteBatch` > Remove many leads. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_delete_leads` | | Convex function | `mcpQueries:deleteLeadBatch` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadIds` | `id<"leads">[]` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.deleteBatch", "args": { "leadIds": "<leadIds>" } }' ``` ```json { "fn": "leads.deleteBatch", "args": { "leadIds": "<leadIds>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:deleteLeadBatch` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/delete --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.delete` > Remove one lead. Deleting is the caller's act, Selda never removes a lead on its own. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_delete_lead` | | Convex function | `mcpQueries:deleteLead` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `id<"leads">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.delete", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "leads.delete", "args": { "leadId": "<leadId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:deleteLead` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/enrich-batch --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.enrichBatch` > Enrich many leads from a natural-language instruction. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `leads/enrichBatch:enrichLeadBatch` | A free sandbox key is refused: it resolves real people's contact details through a paid provider, which costs money per call. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | optional | | | `leadIds` | `string[]` | optional | | | `roles` | `string[]` | optional | | | `language` | `string` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.enrichBatch", "args": {} }' ``` ```json { "fn": "leads.enrichBatch", "args": {} } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `leads/enrichBatch:enrichLeadBatch` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/enrich --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.enrich` `leads.enrich` is dispatched on 2 endpoints. Which one runs depends on the URL you post to, not on the name. ## `run` `leads.enrich` > Enrich one lead from a natural-language instruction. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `leads/enrichBatch:enrichLead` | A free sandbox key is refused: it resolves real people's contact details through a paid provider, which costs money per call. ### Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `string` | **required** | | | `roles` | `string[]` | optional | | | `language` | `string` | optional | | ### What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ### Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.enrich", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "leads.enrich", "args": { "leadId": "<leadId>" } } ``` ### Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `leads/enrichBatch:enrichLead` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. ## `run` `leads.enrich` > Enrich a workspace's leads from a natural-language instruction (legacy path). | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `leads/enrichBatch:enrichBatch` | A free sandbox key is refused: it resolves real people's contact details through a paid provider, which costs money per call. <Callout type="error">**Not reachable.** Another registry table (`INTERNAL_ACTIONS`) declares `leads.enrich` on `/mcp/run` too, and the dispatcher checks that one first. This entry can never be the one that runs. It is documented because it exists in the registry, not because you can call it.</Callout> ### Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `instruction` | `string` | **required** | | | `concurrency` | `number` | optional | | ### What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ### Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.enrich", "args": { "projectId": "<projectId>", "instruction": "<instruction>" } }' ``` ```json { "fn": "leads.enrich", "args": { "projectId": "<projectId>", "instruction": "<instruction>" } } ``` ### Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `leads/enrichBatch:enrichBatch` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/get --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.get` > One lead in full: research, fit, outreach angle, notes. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_lead` | | Convex function | `mcpQueries:getLead` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.get", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "leads.get", "args": { "leadId": "<leadId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getLead` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.list` > Leads in a workspace. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_leads` | | Convex function | `mcpQueries:listLeads` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `limit` | `number` | optional | | | `cursor` | `string` | optional | | | `tags` | `string[]` | optional | | | `city` | `string` | optional | | | `status` | `string` | optional | | | `campaignId` | `string` | optional | | | `hasEmail` | `boolean` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.list", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "leads.list", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:listLeads` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/merge --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.merge` > Merge duplicate leads. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_merge_leads` | | Convex function | `mcpQueries:mergeLead` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `primaryId` | `id<"leads">` | **required** | | | `duplicateId` | `id<"leads">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.merge", "args": { "primaryId": "<primaryId>", "duplicateId": "<duplicateId>" } }' ``` ```json { "fn": "leads.merge", "args": { "primaryId": "<primaryId>", "duplicateId": "<duplicateId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:mergeLead` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/skip --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.skip` > DELETES a lead and every message on it (legacy path, Clerk-authenticated, an API key cannot reach this; use leads.delete instead). | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `leads/mutations:skipLead` | A free sandbox key may call it. <Callout type="warning">A legacy entry. It is dispatched, and its own summary above says what that costs you.</Callout> ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `id<"leads">` | **required** | | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.skip", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "leads.skip", "args": { "leadId": "<leadId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `leads/mutations:skipLead` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/update-status --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.updateStatus` > Set a lead's status (legacy path, Clerk-authenticated, an API key cannot reach this; the MCP tool uses the org-scoped leads.update). | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `leads/mutations:updateLeadStatus` | A free sandbox key may call it. <Callout type="warning">A legacy entry. It is dispatched, and its own summary above says what that costs you.</Callout> ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `id<"leads">` | **required** | | | `status` | `"new" \| "contacted" \| "responded" \| "qualified" \| "quote" \| ...` | **required** | | Shapes that did not fit the table: ```ts status: "new" | "contacted" | "responded" | "qualified" | "quote" | "unqualified" ``` ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.updateStatus", "args": { "leadId": "<leadId>", "status": "new" } }' ``` ```json { "fn": "leads.updateStatus", "args": { "leadId": "<leadId>", "status": "new" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `leads/mutations:updateLeadStatus` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/leads/update --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `leads.update` `leads.update` is dispatched on 2 endpoints. Which one runs depends on the URL you post to, not on the name. ## `mutate` `leads.update` > Edit a lead's fields, including its status. Org-scoped, so an API key can reach it. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_update_lead` | | Convex function | `mcpQueries:updateLeadInternal` | A free sandbox key may call it. ### Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `id<"leads">` | **required** | | | `notes` | `string` | optional | | | `appendNote` | `boolean` | optional | Add `notes` to what is already there instead of replacing it. Default false, because that is what this fn has always done over HTTP and a silent change of meaning would rewrite an integration's behaviour without it asking. The MCP tool `selda_update_lead` passes true unless told otherwise: its own description says "notes to add", and an integration writing a timeline one line at a time was getting a field with one line in it. A timeline that overwrites itself is not a timeline. | | `tags` | `string[]` | optional | | | `status` | `string` | optional | | | `analysis` | `string` | optional | THE ANALYSIS IS CORRECTABLE, because a wrong fact in it is not cosmetic. `add_lead` writes `providedResearch` and nothing could edit it, so an analysis pushed with a false claim, "the price is not shown", when the prices were right there, was permanent, and it is the text Selda writes the message FROM. The wrong fact would have reached the customer with no way to stop it short of deleting the lead. | | `campaignId` | `string` | optional | | | `firstName` | `string` | optional | | | `lastName` | `string` | optional | | | `email` | `string` | optional | | | `jobTitle` | `string` | optional | | | `companyDescription` | `string` | optional | | | `city` | `string` | optional | | | `country` | `string` | optional | | | `location` | `string` | optional | | | `fitScore` | `number` | optional | | | `outreachAngle` | `string` | optional | | | `externalUrl` | `string` | optional | | | `customFields` | `any` | optional | | | `companyWebsite` | `string` | optional | | | `companyDomain` | `string \| null` | optional | | | `dealStage` | `"proposal_sent" \| "negotiation" \| "won" \| "lost" \| null` | optional | Where the deal stands, set by the person or their agent; Selda never advances it itself. | | `dealValueEur` | `number \| null` | optional | What it is worth, in EUR. null clears a wrong figure. | ### What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ### Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.update", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "leads.update", "args": { "leadId": "<leadId>" } } ``` ### Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:updateLeadInternal` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. ## `mutate` `leads.update` > Edit a lead (legacy path, shadowed by the org-scoped leads.update above). | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `leads/mutations:updateLead` | A free sandbox key may call it. <Callout type="error">**Not reachable.** Another registry table (`INTERNAL_MUTATIONS`) declares `leads.update` on `/mcp/mutate` too, and the dispatcher checks that one first. This entry can never be the one that runs. It is documented because it exists in the registry, not because you can call it.</Callout> ### Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `id<"leads">` | **required** | | | `email` | `string` | optional | | | `firstName` | `string` | optional | | | `lastName` | `string` | optional | | | `jobTitle` | `string` | optional | | | `isPurchased` | `boolean` | optional | | | `productFitAnalysis` | `string` | optional | | | `leadSummary` | `string` | optional | | | `valueProposition` | `string` | optional | | | `outreachAngle` | `string` | optional | | | `websiteData` | `any` | optional | | | `enriched` | `boolean` | optional | | | `differentiationAnalysis` | `any` | optional | | | `researchStatus` | `string` | optional | | | `fitScore` | `number` | optional | | | `fitReason` | `string` | optional | | | `whySelected` | `string` | optional | | | `companyDescription` | `string` | optional | | | `companyIndustry` | `string` | optional | | | `companySize` | `string` | optional | | | `bestSalesAngle` | `object` | optional | | | `whyGoodLead` | `{ summary?: string; matchReasons?: string[] }` | optional | | | `pendingMeetingSlots` | `object[]` | optional | | | `pendingMeetingProposalAt` | `number \| null` | optional | | | `notes` | `string` | optional | | | `companyResearch` | `string` | optional | | | `newsSummary` | `string` | optional | | | `googlePlaceId` | `string` | optional | | | `decisionMakers` | `object[]` | optional | | | `city` | `string` | optional | | | `country` | `string` | optional | | | `location` | `string` | optional | | | `companyCity` | `string` | optional | | | `companyCountry` | `string` | optional | | | `companyUrl` | `string` | optional | | | `companyDomain` | `string` | optional | | | `companyWebsite` | `string` | optional | | | `googleMapsCity` | `string` | optional | | | `googleMapsCountry` | `string` | optional | | | `apolloPersonLocation` | `string` | optional | | | `outreachLanguage` | `string` | optional | | | `timezone` | `string` | optional | IANA timezone for meeting labels (Sales Inbox override). | | `company` | `string` | optional | | | `phone` | `string` | optional | | | `tags` | `string[]` | optional | | Shapes that did not fit the table: ```ts bestSalesAngle: { angle: string; reasoning?: string; painPoints?: string[]; opportunities?: string[]; competitorWeaknesses?: string[]; websiteInsights?: string; generatedAt?: number } pendingMeetingSlots: { start: number; end: number; timezone: string; organizerTimezone?: string; provider?: string; label?: string; seldaSlotId?: id<"seldaCalendarSlots"> }[] decisionMakers: { firstName: string; lastName: string; title?: string; confidence: number }[] ``` ### Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "leads.update", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "leads.update", "args": { "leadId": "<leadId>" } } ``` ### Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `leads/mutations:updateLead` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/material/import --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `material.import` > Your prospect folder → a campaign + company list, then it stops. Uploading material is not permission to send. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_import_material` | | Convex function | `campaignRunner/folderImportApi:importProspectMaterial` | A free sandbox key may call it, but not with every argument. See the note below. <Callout type="info">Whether a sandbox key gets through depends on the arguments. Importing your own material costs Selda nothing, so it is free; `autoAdvance` carries the run on into contact lookup and drafting, which spend, so with it the same call needs a live key.</Callout> ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `files` | `object[]` | **required** | | | `assignments` | `{ folderName: string; domain: string }[]` | optional | | | `excludedFolders` | `string[]` | optional | | | `campaignName` | `string` | optional | | | `targetRunId` | `id<"campaignRuns">` | optional | Push INTO an existing campaign instead of creating one (#357), the campaign's run id. The other door onto the same feature: a script that already produced a second folder of prospects for the same motion should be able to add them as the campaign's next wave rather than fragmenting one effort across several campaigns. The permission rule is unchanged, the wave parks at the company-list gate and inherits nothing from an earlier wave's grant, and the run must belong to the project this API key's org owns, which `appendCandidateBatch` verifies. | | `campaignBrief` | `string` | optional | The operator's campaign document (18.8.2026): rules, structure, tone, what may not be claimed. This is what made the API pipeline half-manual, analyses went in over the wire, but the RULES had to be pasted by hand in the app, and a campaign without them runs on house defaults. Honored exactly as the Määritä card's own-prompt mode is. On a `targetRunId` wave it is accepted only when the campaign has no brief yet; a differing one is refused out loud. | | `autoAdvance` | `boolean \| ("leads" \| "messages")[]` | optional | Explicit grant to continue past the company list. A script CAN pass this, Autopilot is a legitimate caller, but it is never the default, and the grant is recorded on the run. Per-stage since #301: `["leads"]` grants contact lookup and stops before any message is written; `true` grants both. A script that only wants the contacts found should say so rather than granting everything, because message writing spends credits. Neither form can send. Sending stays behind `launchRun` and the approval gate. | Shapes that did not fit the table: ```ts files: { path: string; storageId: id<"_storage">; sizeBytes: number; mimeType?: string }[] ``` ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "material.import", "args": { "projectId": "<projectId>", "files": [{}] } }' ``` ```json { "fn": "material.import", "args": { "projectId": "<projectId>", "files": [{}] } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignRunner/folderImportApi:importProspectMaterial` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/material/upload --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `POST /mcp/material/upload` > Raw file bytes in, storageId out. Send the file's path in X-Selda-Path. That path is how Selda maps a file to a company. Then hand the ids to material.import. | | | | --- | --- | | Endpoint | `POST /mcp/material/upload` | | Scope | `pipeline` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_upload_material` | | Convex function | `convex/mcpApi.ts (httpAction)` | A free sandbox key may call it. ## Arguments This endpoint takes the raw bytes of one file as the request body, not a JSON `args` object. | Header | Required | What it is | | --- | --- | --- | | `Authorization` | yes | `Bearer sk_live_...` or `Bearer sk_test_...` | | `X-Selda-Path` | yes | The file's path inside your folder. That path is how Selda maps a file to a company. | ## Example request ```bash curl -X POST https://api.selda.ai/mcp/material/upload \ -H "Authorization: Bearer sk_live_..." \ -H "X-Selda-Path: boreo/filterit/analysis.pdf" \ -H "Content-Type: application/pdf" \ --data-binary @analysis.pdf ``` ## Example response ```json { "value": { "storageId": "<storageId>", "path": "boreo/filterit/analysis.pdf", "sizeBytes": 20481 }, "request_id": "req_..." } ``` --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/messages/approve --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `messages.approve` > Approve a drafted message. Approval only. It does not send. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_approve_message` | | Convex function | `mcpQueries:approveMessage` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `messageId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "messages.approve", "args": { "messageId": "<messageId>" } }' ``` ```json { "fn": "messages.approve", "args": { "messageId": "<messageId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:approveMessage` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/messages/by-lead --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `messages.byLead` > The whole thread with one lead, sent and received. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_thread` | | Convex function | `mcpQueries:getThread` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "messages.byLead", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "messages.byLead", "args": { "leadId": "<leadId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getThread` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/messages/by-project --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `messages.byProject` > Messages in a workspace. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_messages` | | Convex function | `mcpQueries:listMessages` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `limit` | `number` | optional | | | `offset` | `number` | optional | | | `since` | `number` | optional | Epoch ms. Only messages at or after this instant. | | `direction` | `"outbound" \| "inbound"` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "messages.byProject", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "messages.byProject", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:listMessages` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/messages/generate --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `messages.generate` > Draft a message for a lead. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_generate_message` | | Convex function | `mcpQueries:generateMessage` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `string` | **required** | | | `campaignId` | `string` | optional | | | `outreachAngle` | `string` | optional | | | `channel` | `string` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "messages.generate", "args": { "leadId": "<leadId>" } }' ``` ```json { "fn": "messages.generate", "args": { "leadId": "<leadId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:generateMessage` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/projects/get --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `projects.get` > One workspace in full: business context, market analysis, ICP, settings. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_project` | | Convex function | `mcpQueries:getProject` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "projects.get", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "projects.get", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getProject` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/projects/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `projects.list` > Your workspaces. Start here. Every other fn needs a projectId. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_projects` | | Convex function | `mcpQueries:listProjects` | A free sandbox key may call it. ## Arguments This function takes no arguments of its own. Send `"args": {}`. ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | | `userId` | the user the API key belongs to | | `boundProjectId` | the one workspace the key is confined to, when it is confined to one | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "projects.list", "args": {} }' ``` ```json { "fn": "projects.list", "args": {} } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:listProjects` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/projects/update-context --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `projects.updateContext` > Rewrite a workspace's business context. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_update_project_context` | | Convex function | `mcpQueries:updateProjectContext` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `whatTheyDo` | `string` | optional | | | `industry` | `string` | optional | | | `valueProposition` | `string` | optional | | | `products` | `string` | optional | | | `targetMarkets` | `string[]` | optional | | | `jobTitles` | `string[]` | optional | | | `companySize` | `string` | optional | | | `voiceExamplesGood` | `string[]` | optional | | | `voiceExamplesBad` | `string[]` | optional | | | `forbiddenPhrases` | `string[]` | optional | | | `toneWords` | `string[]` | optional | | | `competitors` | `string[]` | optional | | | `knownCustomers` | `string[]` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "projects.updateContext", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "projects.updateContext", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:updateProjectContext` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/replies/classify --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `replies.classify` > Classify inbound replies. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_classify_replies` | | Convex function | `mcpQueries:classifyReplies` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "replies.classify", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "replies.classify", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:classifyReplies` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/replies/draft --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `replies.draft` `replies.draft` is dispatched on 2 endpoints. Which one runs depends on the URL you post to, not on the name. ## `mutate` `replies.draft` > Write a reply draft into a lead's Sales Inbox thread. A person reviews and sends it in the app, this can send nothing. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_draft_reply` | | Convex function | `mcpQueries:draftInboxReply` | A free sandbox key may call it. ### Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `leadId` | `string` | **required** | | | `body` | `string` | **required** | | | `instruction` | `string` | optional | What the person asked for in their own words ("tee tästä lyhyempi"). Optional, kept only in the draft history beside the text, it changes nothing about what is stored in the composer. The `selda_draft_reply` tool schemas in `api/mcp.ts` and `mcp-server/index.ts` do not pass it yet, so this arrives empty on the tool path until they do. | ### What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ### Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "replies.draft", "args": { "leadId": "<leadId>", "body": "<body>" } }' ``` ```json { "fn": "replies.draft", "args": { "leadId": "<leadId>", "body": "<body>" } } ``` ### Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:draftInboxReply` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. ## `run` `replies.draft` > Draft an answer to a reply. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | none, HTTP only | | Convex function | `mcpQueries:draftReply` | A free sandbox key may call it. ### Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `threadId` | `string` | **required** | | | `intent` | `string` | optional | | ### What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ### Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "replies.draft", "args": { "threadId": "<threadId>" } }' ``` ```json { "fn": "replies.draft", "args": { "threadId": "<threadId>" } } ``` ### Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:draftReply` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/replies/preview --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `replies.preview` > Ask how Selda would answer an enquiry, from this workspace's Brain, without creating a lead or storing a draft. Same writer the real reply uses, so tuning against this tunes the real thing. Stores nothing and sends nothing. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_preview_reply` | | Convex function | `salesInbox/inboundAutoDraft:previewInboundReply` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `eventType` | `string` | **required** | What kind of thing arrived: "quote_requested", "guide_downloaded", anything you use. | | `payload` | `any` | optional | The form as you would send it, keys and all. Exactly what `events.ingest` takes. | | `who` | `string` | optional | Who it is from, for the greeting. Optional, an anonymous enquiry is a real case. | | `company` | `string` | optional | | | `source` | `string` | optional | | | `language` | `string` | optional | Force a language instead of letting the enquiry decide. For checking one deliberately. | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "replies.preview", "args": { "projectId": "<projectId>", "eventType": "<eventType>" } }' ``` ```json { "fn": "replies.preview", "args": { "projectId": "<projectId>", "eventType": "<eventType>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `salesInbox/inboundAutoDraft:previewInboundReply` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/runs/archive --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `runs.archive` > Close a campaign run and take it off the active list. Keeps every contact and every message, deleting contacts stays a human act in the app. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_archive_run` | | Convex function | `campaignRunner/mutations:archiveRunFromApiKey` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `runId` | `id<"campaignRuns">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "runs.archive", "args": { "runId": "<runId>" } }' ``` ```json { "fn": "runs.archive", "args": { "runId": "<runId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignRunner/mutations:archiveRunFromApiKey` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/runs/confirm-companies --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `runs.confirmCompanies` > Confirm a run's company list so Selda finds the decision-makers and drafts the messages. Spends credits. Sends nothing, the send is still a human press in the app. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | `selda_confirm_companies` | | Convex function | `campaignRunner/mutations:confirmProfileFromApiKey` | A free sandbox key is refused: it runs the discovery and research engine: web search, crawls, model calls, per-lead credits. <Callout type="warning">A legacy entry. It is dispatched, and its own summary above says what that costs you.</Callout> ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `runId` | `id<"campaignRuns">` | **required** | | | `language` | `string` | **required** | | | `languageChosen` | `boolean` | optional | DID THE HUMAN NAME THAT LANGUAGE at this gate, or is it Selda's proposal riding through? The card sends a language on every confirm, whether or not anyone touched the field, and the engine read the mere presence of `confirmedProfile.language` as "the operator answered the question", then enforced it over every company's own evidence and told them they had chosen it. Absent = an older client that does not send it; the presence of a language is then read as before, and simply never attributed to the human. | | `channel` | `string` | **required** | | | `targetAudience` | `string` | optional | | | `targetRoles` | `string[]` | optional | | | `targetDecisionMakers` | `{ title: string; why?: string }[]` | optional | | | `targetMarket` | `string` | optional | | | `greeting` | `string` | optional | | | `tone` | `string` | optional | | | `leadsPerCompany` | `string` | optional | | | `excludedDomains` | `string[]` | optional | | | `researchCount` | `number` | optional | | | `clarifyingAnswers` | `{ question: string; answer: string }[]` | optional | | | `contactLookups` | `{ phone?: boolean; linkedin?: boolean }` | optional | WHAT THIS SEARCH IS ASKED TO LOOK UP BEYOND AN ADDRESS, the human's answers, #127. Absent = an older client, and then `normalizeContactLookups` supplies the defaults, which are written to reproduce what those runs already did. Neither switch controls KEEPING anything. Every phone number and every profile URL a rung already running happens to publish is stored either way, dropping one to honour a switch would be Selda throwing away a fact nobody asked it to throw away. What they control is whether Selda spends anything going to look. | | `contactResearchRequest` | `string` | optional | What else the human wants found out about the decision-maker, in their own words. A real query, not a note: `engineV3` runs one web search per named contact with exactly this question, stores the answer on the lead and hands it to the composer. Empty = nothing extra is searched for and nothing extra is spent. | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "runs.confirmCompanies", "args": { "runId": "<runId>", "language": "<language>", "channel": "<channel>" } }' ``` ```json { "fn": "runs.confirmCompanies", "args": { "runId": "<runId>", "language": "<language>", "channel": "<channel>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignRunner/mutations:confirmProfileFromApiKey` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/runs/leads --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `runs.leads` > The companies a run found, each with the message Selda drafted for it. Nothing is sent. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_run_leads` | | Convex function | `mcpQueries:getRunLeads` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `runId` | `string` | **required** | | | `limit` | `number` | optional | | | `withDraftOnly` | `boolean` | optional | Only rows that actually carry a draft. Default false, the caller usually wants everything. | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "runs.leads", "args": { "runId": "<runId>" } }' ``` ```json { "fn": "runs.leads", "args": { "runId": "<runId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getRunLeads` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/runs/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `runs.list` > Every campaign run in a project, newest first, with its status. Use it to find a runId you no longer have. Runs the human archived are left out; pass includeArchived: true to see them too. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_runs` | | Convex function | `mcpQueries:listRuns` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `string` | **required** | | | `limit` | `number` | optional | | | `includeArchived` | `boolean` | optional | Show the runs the human archived as well. Default false, see the note in the handler. It exists because this door's whole reason to exist is that a runId must stay findable, and a filter with no way past it would take that back for exactly the runs somebody tidied away. Archiving hides; it has never deleted anything. | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "runs.list", "args": { "projectId": "<projectId>" } }' ``` ```json { "fn": "runs.list", "args": { "projectId": "<projectId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:listRuns` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/runs/rename --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `runs.rename` > Give a campaign run a name a person would recognise. An empty name restores the derived title. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_rename_campaign` | | Convex function | `campaignRunner/mutations:renameRunFromApiKey` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `runId` | `id<"campaignRuns">` | **required** | | | `name` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "runs.rename", "args": { "runId": "<runId>", "name": "<name>" } }' ``` ```json { "fn": "runs.rename", "args": { "runId": "<runId>", "name": "<name>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignRunner/mutations:renameRunFromApiKey` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/runs/start-from-leads --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `runs.startFromLeads` > Start a campaign from leads already pushed in with selda_add_lead, selected by the source label you gave them. No discovery, Selda writes a message per lead from the analysis that came with it, and stops at the drafts. | | | | --- | --- | | Endpoint | `POST /mcp/run` | | Scope | `pipeline` | | Sandbox key | **refused** | | Extra entitlement | none | | MCP tools | `selda_start_campaign_from_leads` | | Convex function | `campaignRunner/importLeads:startRunFromLeadsFromApiKey` | A free sandbox key is refused: it runs the discovery and research engine: web search, crawls, model calls, per-lead credits. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `projectId` | `id<"projects">` | **required** | | | `source` | `string` | **required** | | | `sourceDisplayLabel` | `string` | optional | | | `campaignId` | `id<"campaigns">` | optional | | | `campaignBrief` | `string` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "runs.startFromLeads", "args": { "projectId": "<projectId>", "source": "<source>" } }' ``` ```json { "fn": "runs.startFromLeads", "args": { "projectId": "<projectId>", "source": "<source>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `campaignRunner/importLeads:startRunFromLeadsFromApiKey` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/runs/status --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `runs.status` > Status of one campaign run: phase, companies found, contacts resolved, drafts written, errors. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_get_run_status` | | Convex function | `mcpQueries:getRunStatus` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `runId` | `string` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "runs.status", "args": { "runId": "<runId>" } }' ``` ```json { "fn": "runs.status", "args": { "runId": "<runId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `mcpQueries:getRunStatus` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/webhooks/create --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `webhooks.create` > Register an endpoint for events like reply.received. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_create_webhook` | | Convex function | `integrations/outboundWebhooks:createWebhook` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `url` | `string` | **required** | | | `events` | `string[]` | **required** | | | `description` | `string` | optional | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | | `userId` | the user the API key belongs to | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "webhooks.create", "args": { "url": "<url>", "events": ["<events>"] } }' ``` ```json { "fn": "webhooks.create", "args": { "url": "<url>", "events": ["<events>"] } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `integrations/outboundWebhooks:createWebhook` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/webhooks/delete --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `webhooks.delete` > Remove a webhook endpoint. | | | | --- | --- | | Endpoint | `POST /mcp/mutate` | | Scope | `write` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_delete_webhook` | | Convex function | `integrations/outboundWebhooks:deleteWebhook` | A free sandbox key may call it. ## Arguments | Argument | Type | Required | Notes | | --- | --- | --- | --- | | `webhookId` | `id<"workspaceWebhooks">` | **required** | | ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "webhooks.delete", "args": { "webhookId": "<webhookId>" } }' ``` ```json { "fn": "webhooks.delete", "args": { "webhookId": "<webhookId>" } } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `integrations/outboundWebhooks:deleteWebhook` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/api/webhooks/list --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-api-reference.mjs Regenerate: node scripts/generate-api-reference.mjs convex/lib/__tests__/apiReferenceIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # `webhooks.list` > Outbound webhook endpoints registered for this workspace. | | | | --- | --- | | Endpoint | `POST /mcp/query` | | Scope | `read` | | Sandbox key | allowed | | Extra entitlement | none | | MCP tools | `selda_list_webhooks` | | Convex function | `integrations/outboundWebhooks:listWebhooks` | A free sandbox key may call it. ## Arguments This function takes no arguments of its own. Send `"args": {}`. ## What Selda fills in You never send these. The HTTP layer overwrites them from the API key, which is what keeps one organisation's data out of another's reach. | Argument | Filled in from | | --- | --- | | `orgId` | your organisation, resolved from the API key | ## Example request Required arguments only. Values in angle brackets are yours to fill in; the optional ones are in the table above. ```bash curl -X POST https://api.selda.ai/mcp/query \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "fn": "webhooks.list", "args": {} }' ``` ```json { "fn": "webhooks.list", "args": {} } ``` ## Example response Every endpoint answers in the same envelope. ``` { "value": <the function's return value>, "request_id": "req_..." } ``` **The shape of `value` is not documented here.** `integrations/outboundWebhooks:listWebhooks` declares no `returns` validator, so there is nothing to derive it from, and a shape written out by hand here is a shape that goes stale without anything noticing. Call it once against a test key and read what comes back. On failure the body is `{ "error": { "type", "code", "message", "request_id" }, "request_id" }` and `request_id` is echoed in the `X-Request-Id` header. --- Generated from `convex/lib/mcpRegistry.ts`. Nothing in this reference sends: `launchRun` is in no registry and never will be, so a script can prepare a campaign completely and a person still presses send in the app. --- <!-- https://docs.selda.ai/reference/rest --> {/* GENERATED FILE. Do not edit by hand. Source: convex/lib/mcpRegistry.ts -> scripts/generate-rest-api.mjs Regenerate: node scripts/generate-rest-api.mjs convex/lib/__tests__/restApiIsGenerated.test.ts fails if this file drifts. */} import { Callout } from "nextra/components"; # REST paths Every path below is a different URL for a call the API already had. The request is answered by the same dispatcher that answers `POST /mcp/query`, `POST /mcp/mutate` and `POST /mcp/run`, with the same authentication, the same scope check, the same organisation scoping and the same refusals. A path cannot reach anything the RPC form refuses, because it is the RPC form underneath. ``` https://api.selda.ai/v1/... Authorization: Bearer sk_live_... ``` ## The OpenAPI document [`openapi.json`](https://docs.selda.ai/openapi.json) is OpenAPI 3.1, generated from the same registry. Import it into Postman, or point a client generator at it. <Callout type="info">**Response shapes are almost never in it.** A shape for `value` is published for 1 of the 71 operations, because one is derived only where the target function declares a `returns` validator. Everywhere else `value` carries a description saying exactly that, rather than an invented object. A generated client that enforces a shape nobody derived is worse than one that enforces nothing. Call an operation once with a test key and read what comes back.</Callout> ## How a name becomes a path The rules are mechanical, so a new registry entry gets its path without anybody choosing one. - **The method comes from the endpoint the dispatcher already routes the function to**, not from its name. Everything on `/mcp/query` runs a Convex query, which structurally cannot write, so `GET` is safe by construction. Everything else changes something or spends something and is never a `GET`. - **An identifier reaches the path only when it is the group's own.** `leads.get` takes a `leadId`, so it is `GET /v1/leads/{leadId}`. `leads.list` takes a required `projectId`, which is a filter and not the address of a lead, so it stays a query parameter and the path is `GET /v1/leads`. - **The name segment is dropped only where the method already says it**: `list` and `get` under `GET`, `add` and `create` under `POST`. Everything else keeps its verb. - **An action keeps its verb.** `runs.confirmCompanies` is `POST /v1/runs/{runId}/confirm-companies`. It is a thing you do to a run, not a field of one, and dressing it up as a `PATCH` of some invented status would read as idempotent while it starts paid work. - `GET` carries its arguments in the query string and everything else takes a JSON body. Repeat a parameter for a list: `?tags=a&tags=b`. ## Every path 71 routes across 18 groups, plus the upload door. ### `brain` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/brain` | [`brain.list`](/reference/api/brain/list) | The workspace's structured knowledge: products, partners, references, company facts, and the things Selda must never say. Each item has a type, a title and a body. | | `POST` | `/v1/brain` | [`brain.add`](/reference/api/brain/add) | Add one thing Selda should know: a product, a partner, a reference, a company fact, a note, something it must never say, or a `writing_rule` — a standing instruction about HOW messages are written, which reaches the composer as a directive and is never quoted as material. | | `POST` | `/v1/brain/remove` | [`brain.remove`](/reference/api/brain/remove) | Take one Brain item back out. The human owns what Selda knows. | | `POST` | `/v1/brain/update` | [`brain.update`](/reference/api/brain/update) | Rewrite the title and body of one Brain item. | ### `campaigns` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/campaigns` | [`campaigns.list`](/reference/api/campaigns/list) | Campaigns in a workspace. | | `POST` | `/v1/campaigns` | [`campaigns.create`](/reference/api/campaigns/create) | Create a campaign (legacy table, not the one the app's campaign-flow UI reads). | | `GET` | `/v1/campaigns/{campaignId}` | [`campaigns.get`](/reference/api/campaigns/get) | One campaign: status, channels, settings, leads. | | `PATCH` | `/v1/campaigns/{campaignId}` | [`campaigns.update`](/reference/api/campaigns/update) | Change a campaign. | | `POST` | `/v1/campaigns/{campaignId}/add-leads` | [`campaigns.addLeads`](/reference/api/campaigns/add-leads) | Put specific leads into a campaign. | | `POST` | `/v1/campaigns/{campaignId}/lock-message-structure` | [`campaigns.lockMessageStructure`](/reference/api/campaigns/lock-message-structure) | Lock a campaign's message structure so every locked block ships exactly as written and nothing rewrites it, or unlock it with locked: false. Sends nothing. | | `GET` | `/v1/campaigns/{campaignId}/message-structure` | [`campaigns.messageStructure`](/reference/api/campaigns/message-structure) | Read what a campaign's message is made of: every block, which ones ship verbatim, the instruction behind each generated one, the shape, and whether it is locked. | | `POST` | `/v1/campaigns/{campaignId}/set-message-structure` | [`campaigns.setMessageStructure`](/reference/api/campaigns/set-message-structure) | State what a campaign's message is made of: blocks that ship WORD FOR WORD, blocks Selda writes from an instruction you give it, the paragraph count, and what must never appear. Refuses to change a locked structure. Sends nothing. | | `GET` | `/v1/campaigns/{campaignId}/stats` | [`campaigns.stats`](/reference/api/campaigns/stats) | Campaign counters: sent, delivered, opened, clicked, replied, bounced. | | `POST` | `/v1/campaigns/add-leads-by-tag` | [`campaigns.addLeadsByTag`](/reference/api/campaigns/add-leads-by-tag) | Put every lead carrying a tag into a campaign (legacy table). | | `POST` | `/v1/campaigns/add-rule` | [`campaigns.addRule`](/reference/api/campaigns/add-rule) | Add a campaign rule. | ### `company` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/company/lookup` | [`company.lookup`](/reference/api/company/lookup) | Resolve a company and return the right people to reach. Starts nothing. (live key only) | ### `connectors` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/connectors` | [`connectors.list`](/reference/api/connectors/list) | Data connectors registered for this workspace. | | `POST` | `/v1/connectors` | [`connectors.create`](/reference/api/connectors/create) | Register a data connector. | | `DELETE` | `/v1/connectors/{connectorId}` | [`connectors.delete`](/reference/api/connectors/delete) | Remove a data connector. | | `POST` | `/v1/connectors/{connectorId}/sync` | [`connectors.sync`](/reference/api/connectors/sync) | Pull from a connected data source. (live key only) | ### `credits` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/credits/info` | [`credits.info`](/reference/api/credits/info) | Credit balance, daily free credits, usage, plan. | ### `drafts` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/drafts/remove` | [`drafts.remove`](/reference/api/drafts/remove) | Take one draft out of a run so it cannot be sent. The row stays visible with your reason and the app can put it back. Refuses a message that already went out. | | `POST` | `/v1/drafts/update` | [`drafts.update`](/reference/api/drafts/update) | Rewrite the draft on one run lead. Refuses a message that already went out; never sends. | ### `engine` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/engine/start` | [`engine.start`](/reference/api/engine/start) | The full pipeline from a brief: find companies → research → fit → hook → draft. It STOPS at the company list (run status `awaiting_profile`) and waits for a person to confirm the companies and the decision-maker roles in the Selda app. Poll `runs.status` and read `awaitingHuman`. Nothing is ever sent from here. (live key only) | ### `events` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/events/ingest` | [`events.ingest`](/reference/api/events/ingest) | Report that something happened outside Selda (a form, an analysis, an ad response). Creates the lead if new, recognises it if known, records it on the timeline, and can put it on a campaign's review list. Pass autoAdvance to have Selda write the reply from the Brain straight away and leave it in the Sales Inbox, draft.ready is published when it is there. It never sends. (needs `inboundIntake`) | ### `flows` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/flows` | [`flows.list`](/reference/api/flows/list) | The flows in a workspace: what runs when something arrives from outside, the steps in order, and whether each is switched on. Includes the workspace's flow instruction files. | | `POST` | `/v1/flows` | [`flows.create`](/reference/api/flows/create) | Create a flow: a trigger plus the steps to run when something arrives. Off unless you say otherwise. No step can send. | | `DELETE` | `/v1/flows/{flowId}` | [`flows.delete`](/reference/api/flows/delete) | Delete a flow and its run log. | | `PATCH` | `/v1/flows/{flowId}` | [`flows.update`](/reference/api/flows/update) | Rewrite a flow's name, trigger or steps. | | `GET` | `/v1/flows/{flowId}/runs` | [`flows.runs`](/reference/api/flows/runs) | What a flow actually did, run by run, step by step, including the steps that did nothing and why. | | `POST` | `/v1/flows/{flowId}/set-enabled` | [`flows.setEnabled`](/reference/api/flows/set-enabled) | Switch a flow on or off. | | `POST` | `/v1/flows/save-skill` | [`flows.saveSkill`](/reference/api/flows/save-skill) | Write or rewrite an instruction file a flow step reads: how this business decides what an enquiry is. | ### `inbox` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/inbox/add-message` | [`inbox.addMessage`](/reference/api/inbox/add-message) | Put one message into a lead's Sales Inbox thread, in either direction, even for somebody who was never in a campaign. Creates the lead if it is new. This can send nothing. | ### `knowledge` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/knowledge` | [`knowledge.get`](/reference/api/knowledge/get) | What Selda knows about your business: the prose that grounds every message. | | `POST` | `/v1/knowledge/append` | [`knowledge.append`](/reference/api/knowledge/append) | Add to what Selda knows about your business. | | `POST` | `/v1/knowledge/set` | [`knowledge.set`](/reference/api/knowledge/set) | Replace what Selda knows about your business. | ### `leads` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/leads` | [`leads.list`](/reference/api/leads/list) | Leads in a workspace. | | `POST` | `/v1/leads` | [`leads.add`](/reference/api/leads/add) | Add one company/contact. Pass `analysis` with research you already did and the message is written from it instead of a fresh crawl. | | `DELETE` | `/v1/leads/{leadId}` | [`leads.delete`](/reference/api/leads/delete) | Remove one lead. Deleting is the caller's act, Selda never removes a lead on its own. | | `GET` | `/v1/leads/{leadId}` | [`leads.get`](/reference/api/leads/get) | One lead in full: research, fit, outreach angle, notes. | | `PATCH` | `/v1/leads/{leadId}` | [`leads.update`](/reference/api/leads/update) | Edit a lead's fields, including its status. Org-scoped, so an API key can reach it. | | `POST` | `/v1/leads/{leadId}/add-alias` | [`leads.addAlias`](/reference/api/leads/add-alias) | Claim another email address for a lead, so a reply from it lands in the same conversation. Also adopts that address's earlier unlinked inbound. | | `POST` | `/v1/leads/{leadId}/add-tag` | [`leads.addTag`](/reference/api/leads/add-tag) | Tag a lead. | | `POST` | `/v1/leads/{leadId}/enrich` | [`leads.enrich`](/reference/api/leads/enrich) | Enrich one lead from a natural-language instruction. (live key only) | | `POST` | `/v1/leads/{leadId}/skip` | [`leads.skip`](/reference/api/leads/skip) | DELETES a lead and every message on it (legacy path, Clerk-authenticated, an API key cannot reach this; use leads.delete instead). | | `PATCH` | `/v1/leads/{leadId}/status` | [`leads.updateStatus`](/reference/api/leads/update-status) | Set a lead's status (legacy path, Clerk-authenticated, an API key cannot reach this; the MCP tool uses the org-scoped leads.update). | | `POST` | `/v1/leads/add-batch` | [`leads.addBatch`](/reference/api/leads/add-batch) | Add many companies/contacts in one call. | | `POST` | `/v1/leads/delete-batch` | [`leads.deleteBatch`](/reference/api/leads/delete-batch) | Remove many leads. | | `POST` | `/v1/leads/enrich-batch` | [`leads.enrichBatch`](/reference/api/leads/enrich-batch) | Enrich many leads from a natural-language instruction. (live key only) | | `POST` | `/v1/leads/merge` | [`leads.merge`](/reference/api/leads/merge) | Merge duplicate leads. | ### `material` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/material/import` | [`material.import`](/reference/api/material/import) | Your prospect folder → a campaign + company list, then it stops. Uploading material is not permission to send. | ### `messages` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/messages/{messageId}/approve` | [`messages.approve`](/reference/api/messages/approve) | Approve a drafted message. Approval only. It does not send. | | `GET` | `/v1/messages/by-lead` | [`messages.byLead`](/reference/api/messages/by-lead) | The whole thread with one lead, sent and received. | | `GET` | `/v1/messages/by-project` | [`messages.byProject`](/reference/api/messages/by-project) | Messages in a workspace. | | `POST` | `/v1/messages/generate` | [`messages.generate`](/reference/api/messages/generate) | Draft a message for a lead. | ### `projects` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/projects` | [`projects.list`](/reference/api/projects/list) | Your workspaces. Start here. Every other fn needs a projectId. | | `GET` | `/v1/projects/{projectId}` | [`projects.get`](/reference/api/projects/get) | One workspace in full: business context, market analysis, ICP, settings. | | `PATCH` | `/v1/projects/{projectId}/context` | [`projects.updateContext`](/reference/api/projects/update-context) | Rewrite a workspace's business context. | ### `replies` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `POST` | `/v1/replies/classify` | [`replies.classify`](/reference/api/replies/classify) | Classify inbound replies. | | `POST` | `/v1/replies/draft` | [`replies.draft`](/reference/api/replies/draft) | Write a reply draft into a lead's Sales Inbox thread. A person reviews and sends it in the app, this can send nothing. | | `POST` | `/v1/replies/preview` | [`replies.preview`](/reference/api/replies/preview) | Ask how Selda would answer an enquiry, from this workspace's Brain, without creating a lead or storing a draft. Same writer the real reply uses, so tuning against this tunes the real thing. Stores nothing and sends nothing. | ### `runs` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/runs` | [`runs.list`](/reference/api/runs/list) | Every campaign run in a project, newest first, with its status. Use it to find a runId you no longer have. Runs the human archived are left out; pass includeArchived: true to see them too. | | `POST` | `/v1/runs/{runId}/archive` | [`runs.archive`](/reference/api/runs/archive) | Close a campaign run and take it off the active list. Keeps every contact and every message, deleting contacts stays a human act in the app. | | `POST` | `/v1/runs/{runId}/confirm-companies` | [`runs.confirmCompanies`](/reference/api/runs/confirm-companies) | Confirm a run's company list so Selda finds the decision-makers and drafts the messages. Spends credits. Sends nothing, the send is still a human press in the app. (live key only) | | `GET` | `/v1/runs/{runId}/leads` | [`runs.leads`](/reference/api/runs/leads) | The companies a run found, each with the message Selda drafted for it. Nothing is sent. | | `POST` | `/v1/runs/{runId}/rename` | [`runs.rename`](/reference/api/runs/rename) | Give a campaign run a name a person would recognise. An empty name restores the derived title. | | `GET` | `/v1/runs/{runId}/status` | [`runs.status`](/reference/api/runs/status) | Status of one campaign run: phase, companies found, contacts resolved, drafts written, errors. | | `POST` | `/v1/runs/start-from-leads` | [`runs.startFromLeads`](/reference/api/runs/start-from-leads) | Start a campaign from leads already pushed in with selda_add_lead, selected by the source label you gave them. No discovery, Selda writes a message per lead from the analysis that came with it, and stops at the drafts. (live key only) | ### `webhooks` | Method | Path | `fn` | What it does | | --- | --- | --- | --- | | `GET` | `/v1/webhooks` | [`webhooks.list`](/reference/api/webhooks/list) | Outbound webhook endpoints registered for this workspace. | | `POST` | `/v1/webhooks` | [`webhooks.create`](/reference/api/webhooks/create) | Register an endpoint for events like reply.received. | | `DELETE` | `/v1/webhooks/{webhookId}` | [`webhooks.delete`](/reference/api/webhooks/delete) | Remove a webhook endpoint. | ### material upload | Method | Path | What it does | | --- | --- | --- | | `POST` | `/v1/material/upload` | Raw file bytes in, a `storageId` out. Send the file's path in `X-Selda-Path`. Then hand the ids to `material.import`. | ## What has no path, and why Three registry entries get no REST route. Each is still callable exactly as it is today, in the RPC form, and nothing about them changed. | `fn` | Call it here instead | Why | | --- | --- | --- | | `leads.enrich` | `POST /mcp/run` | another registry table (INTERNAL_ACTIONS) declares leads.enrich on /mcp/run too and the dispatcher checks that one first, so this entry can never be the one that runs | | `leads.update` | `POST /mcp/mutate` | another registry table (INTERNAL_MUTATIONS) declares leads.update on /mcp/mutate too and the dispatcher checks that one first, so this entry can never be the one that runs | | `replies.draft` | `POST /mcp/run` | `POST /v1/replies/draft` is already the route for `replies.draft` on `/mcp/mutate`, and one URL cannot mean two functions. Call this one at `POST /mcp/run` with `{ "fn": "replies.draft" }` | **And there is still no path that sends.** `launchRun` is in no registry, so there is nothing for a route to be generated from. A script can prepare a campaign completely and a person presses send in the app. --- Generated from `convex/lib/mcpRegistry.ts`. The arguments each path takes are on the function's own page under [Reference / API functions](/reference/api). --- <!-- https://docs.selda.ai/reference/mcp --> import { Callout } from "nextra/components"; import { CopyForAI } from "../../components/CopyForAI"; # Selda MCP Server The Selda MCP server lets external AI tools (Claude Desktop, Claude Code, ChatGPT, or any [Model Context Protocol](https://modelcontextprotocol.io) client) drive a user's Selda workspace: list projects, inspect leads and campaigns, read threads, and run the GTM pipeline. It is gated behind an API key. **Every plan can connect with a test key** (test mode, the full product, nothing sends for real); a **live key requires a paid plan (Pro and up)**. <CopyForAI text={`Selda MCP server reference, see https://docs.selda.ai/reference/mcp Hosted MCP server: https://mcp.selda.ai/api/mcp (Streamable HTTP, add as a custom connector in Claude.ai / ChatGPT / Cursor; the server advertises OAuth, so the client handles login automatically, no key to paste). initialize echoes your protocol version if it is 2025-06-18, 2025-03-26 or 2024-11-05. For ChatGPT specifically the server exposes the two tools OpenAI's connector contract requires: search({query}) -> {results:[{id,title,url}]} and fetch({id}) -> {id,title,text,url,metadata}, a read-only view over leads, projects and Brain items; ids are lead:… / project:… / brain:… and every id search returns is one fetch accepts. For a static key instead (Claude Code, scripts), pass Authorization: Bearer sk_live_… . A local stdio server also exists (mcp-server/index.ts) for local dev, with a SUBSET of the tools, not the same set. Both forward to the HTTP API: POST to the deployment's .convex.site host at /mcp/query (reads), /mcp/mutate (writes), /mcp/run (pipeline/actions). Body is always { fn, args }. Auth: Authorization: Bearer sk_live_… (org-scoped by the key; never send orgId). Every plan can connect with a TEST key (test mode, nothing sends for real). A LIVE key requires a paid plan (Pro and up); a live key on a lower tier gets a 403 with { error: { code: "plan_not_eligible" } }. To push research you already have: POST each file to /mcp/material/upload with an X-Selda-Path header (the path is how Selda maps a file to a company, e.g. "boreo/filterit/analyysi.pdf"), then call material.import on /mcp/run with the returned storageIds. That creates the campaign and company list and STOPS there; pushing material is not permission to send. autoAdvance grants further stages per stage (["leads"] = contact lookup only, true = also message writing), and even then cannot send, launchRun is behind human approval and has no fn. Alternatively leads.add with an "analysis" field grounds a message on your own research without any file upload. Read the full docs page for the tool list, scopes, org-isolation guarantees, and the connect steps, then paste it back with your question.`} label="Copy MCP reference for AI" hint="Paste into Claude or ChatGPT alongside your question" /> The Selda web app is at `https://app.selda.ai`. --- ## Transport, in one answer Read this first if you are writing an integration, because the rest of the page describes three things and you only need one of them. **Use `https://mcp.selda.ai/api/mcp`.** It speaks **MCP Streamable HTTP** over `POST`, JSON-RPC 2.0 in the body. It is **stateless**: there is no session to establish, `Mcp-Session-Id` is neither required nor issued, and responses come back as **plain JSON, never SSE**. `GET` on the same URL returns a health object carrying the server version, which is the only thing here you can read without a key. `DELETE` is accepted and does nothing. **Protocol version.** `initialize` echoes the version you asked for when it is one of `2025-06-18`, `2025-03-26` or `2024-11-05`, and answers with the newest otherwise. Nothing on this server is version-specific, so pick whichever your client speaks. Authenticate either way: - `?key=sk_live_...` in the query string, or `Authorization: Bearer sk_live_...` - or OAuth: the server publishes `/.well-known/oauth-protected-resource`, so Claude.ai, ChatGPT and Cursor can log you in with no key to paste **Connecting from ChatGPT** works through the same URL and the same OAuth discovery. ChatGPT additionally requires a server to expose two tools named `search` and `fetch`, with fixed shapes; Selda serves both, and they are described under [Tools](#tools). If you are writing a **script** rather than connecting an AI client, skip MCP entirely and call the HTTP API directly: `POST` to the deployment's `.convex.site` host with a body of `{ fn, args }`. That is what both MCP servers do underneath, and it is simpler than speaking JSON-RPC. ## The pieces There are two MCP servers in front of one HTTP API, and their toolsets are **not** the same size: the hosted server serves every tool, the local stdio server serves a subset. The exact split is in the generated table under [Tools](#tools). Unless you specifically need a local process, use the hosted one. 1. **The hosted MCP server** (recommended), `https://mcp.selda.ai/api/mcp`, implemented in `api/mcp.ts`, runs on Vercel Edge, speaks MCP Streamable HTTP, stateless. Connect it from Claude.ai (**Add custom connector**) or any remote-MCP client with `https://mcp.selda.ai/api/mcp?key=sk_live_xxx`. No local install. 2. **The local Node MCP server**, `mcp-server/index.ts`. A stdio MCP server spawned by the client (e.g. Claude Desktop via `npx tsx`) that forwards each tool call over HTTP to the same backend. Both call **the Convex HTTP endpoints**, defined in `convex/mcpApi.ts` and routed in `convex/http.ts`: | Endpoint | Method | Purpose | |----------|--------|---------| | `/mcp/query` | `POST` | Read operations (queries) | | `/mcp/mutate` | `POST` | Write operations (mutations) | | `/mcp/run` | `POST` | Actions (pipeline / engine) | | `/mcp/material/upload` | `POST` | Raw file bytes in, `storageId` out, intake for `material.import` | Each also has an `OPTIONS` route for CORS preflight. The base URL is the deployment's `.convex.site` host (prod default: `https://brave-buzzard-349.eu-west-1.convex.site`, overridable via `SELDA_API_URL` / `VITE_CONVEX_SITE_URL`). The HTTP body is always `{ fn, args }`, where `fn` is a registry key (e.g. `projects.list`) and the handler returns `{ value }` on success or `{ error }` with a non-200 status on failure. --- ## Authentication & scoping - **API key format:** `sk_live_…` (prefix + 40 random alphanumeric chars). - **Transport:** sent as a Bearer token: `Authorization: Bearer sk_live_…`. The Node server reads it from the `SELDA_API_KEY` env var. - **Creation:** keys are minted via `apiKeys.createKey` (called from the app UI: Settings → Apps → Selda MCP). The full plaintext key is returned **once** at creation and never stored or shown again. - **Storage:** only a **SHA-256 hash** of the key (`keyHash`) plus a display `keyPrefix` (first 16 chars + `...`) are persisted in the `apiKeys` table. The plaintext key is never written to the database. - **Validation:** every HTTP call runs `apiKeys.validateKey` over the request's hashed token. A key is rejected if it is missing/expired, revoked (`revokedAt`), the owning user is deleted/disabled, or the owning org no longer exists. On success it returns the key's `orgId`, `userId`, and `scopes`. - **Revocation:** `apiKeys.revokeKey` (UI) sets `revokedAt`; the key stops validating immediately. ### Scopes | Scope | Grants | Self-grantable | |-------|--------|----------------| | `read` | `/mcp/query` (and required by all read calls) | Yes | | `write` | `/mcp/mutate` | Yes | | `pipeline` | `/mcp/run` (engine / pipeline actions) | Yes | | `admin` | cross-org admin queries (all users/orgs/projects/stats) | **No** | Default scopes for a newly created key are `["read", "write", "pipeline"]`. The `admin` scope exists for internal cross-org tooling and is **not** granted through normal self-service key creation (security-hardened, see the internal security notes). ### Org isolation Each HTTP handler: 1. checks the required scope for that endpoint (e.g. `/mcp/query` requires `read`), returning `403` if missing; 2. injects the key's `orgId` (and, where relevant, `userId`) **server-side** into the called function's args. Because the org id comes from the validated key, never from the request body, a key can only ever reach data belonging to its own organization. Every underlying `mcpQueries.*` function re-verifies org ownership (e.g. a lead is only returned if its project's `orgId` matches the key's org). One org's key cannot read or mutate another org's projects, leads, campaigns, or messages. --- ## What you can actually do The MCP tools below are a curated front end. Underneath, the HTTP API exposes the **full registry**, every `fn` you can pass to `{ fn, args }`. If a tool doesn't exist for what you want, the `fn` probably does. Reach for this list when you are writing a script rather than chatting with a client. Every call is scoped to the API key's organization. **Never send `orgId`**, it is injected server-side from the validated key, and a value in the body is ignored. ### Push your own research in You already produce per-prospect research somewhere else (a Claude Code pipeline, a scraper, a consultant's PDFs). Two ways to get it in: | I have | Use | |---|---| | **Files on disk**, laid out one folder per company | `/mcp/material/upload` per file, then `material.import` | | **A company + contact + my own analysis text** | `leads.add` with `analysis` | **Folder shape is the mapping.** The path you send in `X-Selda-Path` is how Selda knows which company a file belongs to. `boreo/filterit/analyysi.pdf` means "this belongs to Filterit". Send the path relative to the folder you'd otherwise have dragged into the app. ```bash # 1. One upload per file. Keep each returned storageId. curl -X POST https://api.selda.ai/mcp/material/upload \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "X-Selda-Path: boreo/filterit/analyysi.pdf" \ -H "Content-Type: application/pdf" \ --data-binary @analyysi.pdf # → { "value": { "storageId": "…", "path": "…", "sizeBytes": 20481 }, "request_id": "req_…" } # 2. Turn them into one campaign. curl -X POST https://api.selda.ai/mcp/run \ -H "Authorization: Bearer $SELDA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"fn":"material.import","args":{ "projectId":"<workspace id>", "files":[ {"path":"boreo/filterit/analyysi.pdf","storageId":"…","sizeBytes":20481,"mimeType":"application/pdf"}, {"path":"boreo/tornokone/tiedot.md","storageId":"…","sizeBytes":512,"mimeType":"text/markdown"} ]}}' ``` Optional args: `assignments` (`[{ folderName, domain }]` when you know a company's domain and don't want Selda guessing), `excludedFolders`, `campaignName`, `autoAdvance`. ## The message structure A campaign can carry a document that decides what its messages are made of. Without one, messages are written from the brief alone. ```bash curl -X POST https://api.selda.ai/mcp/mutate \ -H "Authorization: Bearer $SELDA_API_KEY" -H "Content-Type: application/json" \ -d '{"fn":"campaigns.setMessageStructure","args":{ "campaignId":"...", "blocks":[ {"kind":"generated","role":"subject","aiInstruction":"Name the one thing you found on their site."}, {"kind":"generated","role":"body","label":"Observation","aiInstruction":"State what you verified about them. One observation, plainly, no consequence clause."}, {"kind":"locked","role":"body","label":"Offer","content":"Ensimmäinen vaihe on 380 euroa, alv 0 %. Jos työ ei ole valmis kahdessa viikossa, en laskuta."}, {"kind":"generated","role":"body","label":"Ask","aiInstruction":"Ask whether the direction looks right. Never propose a meeting."} ], "form":{"paragraphs":6}, "mustNever":["Never call it an audit","Never name another customer"]}}' ``` **`locked` ships word for word.** Nothing rewrites it, and the composer assembles around it. **`generated` carries your instruction** and Selda writes that part from real material. The one thing an instruction cannot do is make a claim true: where it asks for an observation the research does not establish, that part is left out rather than invented. `campaigns.lockMessageStructure` with `locked: true` freezes the whole document; `false` takes it back to draft, because a lock the operator cannot undo is Selda refusing them their own document. Neither call sends anything. A structure decides how a message is written; a person still approves every send in the app. `material.import` **creates the campaign and the company list and stops there.** Pushing material is permission to read the material, not to run a campaign, the same rule as dropping a folder in the app. From there Selda proposes who to reach at each company, justified from what you sent, and a human accepts each one. `autoAdvance` is how a script grants further stages, and it is per stage: | Value | Grants | |---|---| | omitted | nothing, stops at the company list | | `["leads"]` | contact lookup, then stops before any message is written | | `true` | contact lookup **and** message writing (this spends credits) | Neither form can send. Sending is `launchRun` behind human approval, and there is no `fn` for it. Limits: 25 MB per file, and the same per-key rate limit as every other call. A `projectId` outside your key's organization returns the same "no such workspace" error as one that doesn't exist. ### The full registry Every function, one page each, with its endpoint, its scope, whether a free test key can call it, what it takes and an example request: - **[API functions](/reference/api)** in the sidebar, one entry per `fn` with its endpoint badge - **[Function reference](/connect-your-app/reference)** for the whole list on one page Those pages are GENERATED from `convex/lib/mcpRegistry.ts`, the same table these endpoints dispatch from, and a test fails if they drift from it. The tables that used to sit here were written by hand, and a hand-written copy of a registry is a copy that goes wrong in both directions at once: the API page it replaces listed nineteen functions that are in no registry and was missing thirty-nine that are. The API also serves the list itself, no key needed: ``` GET https://api.selda.ai/mcp/capabilities ``` The same list appears in the app under **Settings, Integrations, MCP server**. --- ## Webhooks The other half of the loop. Register an endpoint with `webhooks.create` and Selda POSTs to it, instead of you polling `messages.byLead` on a timer. ### Events | event | fires when | |---|---| | `draft.ready` | a draft is written and waiting for someone to approve it. This is the one that makes the loop work: it is how you learn there is something to look at, instead of polling | | `draft.failed` | drafting was attempted and produced nothing. Carries the reason, so a silent gap is never mistaken for a quiet week | | `reply.received` | someone answers one of your messages | | `meeting.booked` | a meeting is booked | | `lead.added` | a lead is added, including by your own `leads.add` | | `lead.status_changed` | a lead's status changes | | `message.sent` | a message goes out, after a human approved it | | `campaign.completed` | a campaign run finishes | | `credits.low` | the balance is running out | | `test.ping` | you asked for a test delivery | ### What arrives `POST` to your URL, `Content-Type: application/json`, with two headers: | header | value | |---|---| | `X-Selda-Event` | the event name, e.g. `reply.received` | | `X-Selda-Signature` | `sha256=<hex>`, HMAC-SHA256 of the raw body, keyed with your webhook secret | The body is always the same envelope: ```json { "event": "reply.received", "timestamp": 1786174818273, "data": { } } ``` ### Verifying the signature Sign the **raw body bytes**, before any JSON parsing, a re-serialized object will not match. ```js import crypto from "node:crypto"; const expected = "sha256=" + crypto.createHmac("sha256", process.env.SELDA_WEBHOOK_SECRET) .update(rawBody) // the raw string, not JSON.parse(...) .digest("hex"); // Constant-time compare, never === const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)); ``` ### Delivery, retries and disabling - **3 attempts** per event, with a **10 second** timeout each - Retry delays: **30 seconds**, then **5 minutes**. Byte-identical body and signature on every attempt, so a retry verifies exactly like the first try - Any 2xx counts as delivered. Anything else is a failure - After **10 consecutive failures** the endpoint is disabled rather than retried forever. `webhooks.list` shows `lastStatus`, `failureCount` and `disabledAt`, so you can see it happened <Callout type="info"> Your endpoint should answer fast and do the work afterwards. Ten seconds is the whole budget, and a slow 200 is treated the same as a timeout. </Callout> ### Zapier, Make and n8n These do not speak MCP and do not need to. Point `webhooks.create` at a Zapier **Catch Hook** (or a Make/n8n webhook node) and events arrive as plain JSON. In the other direction, **Webhooks by Zapier** can `POST` to `https://api.selda.ai/mcp/mutate` with an `Authorization: Bearer sk_…` header and a `{"fn":"leads.add","args":{…}}` body. No integration to install on either side. --- ## What MCP does not do This is the shortest section and the most important one. Everything below is a deliberate absence, not a gap waiting to be filled, and none of it is coming later. ### It cannot send **There is no `fn` that sends a campaign.** Sending is `launchRun`, and `launchRun` is not in any registry, has no tool wrapper, and never will be. A script can go all the way to drafted messages sitting in review. A person opens Selda and presses send. This is the product, not a limitation of the API. Anyone can build a thing that mails 10,000 strangers overnight; the reason Selda's messages get replies is that a human looked at each one before it went. An API that could bypass that would be selling something different. Nothing here changes it: not a parameter, not a flag, not a header, not a plan, not an add-on. `selda_ingest_event` can create a lead and put it on a live campaign, and that lead still waits in review with no message written. If you find a way to make Selda send from a script, that is a bug and we want to hear about it. ### It cannot grant itself permission `autoAdvance` is a value you pass. It is recorded on the run and visible in the app, so a person can see that a script asked to go further and how far. There is no call that turns a gate off, and no call that approves a draft on a human's behalf. (`messages.approve` marks a draft approved; it still does not send it.) ### It cannot create a workspace A workspace is set up by a person in the Selda app. There is no `fn` for it, by any key: it was removed rather than gated, because a rule enforced by a plan tier is a rule with a price on it. Once the workspace exists, everything **inside** it can be built and grown from a script: the Brain, the knowledge, leads, your own material, campaigns. ### It cannot read another organization Every call is scoped to the key's organization, injected server side from the validated key. An `orgId` in your request body is ignored. The `admin` scope is cross organization, exists for internal tooling, and is never issued by self service key creation. ### A free test key cannot make the engine find people A test key is free on every plan and gives you the whole product to build against. It can read everything in the workspace and write your own data in. It cannot run the parts that spend real money to discover or enrich a real person. See [Test keys and live keys](#test-keys-and-live-keys). --- ## The Brain: what Selda knows about your business The Brain is the **structured** half of a workspace's knowledge: products, partners, references and proof, company facts, notes, and the hard rules Selda must never break. (`knowledge.get` / `knowledge.set` are the other half, free prose.) Both feed every message Selda writes. This is what makes it possible to stand a workspace up from a script. The workspace itself is created by a person in the app; everything inside it can be filled and grown from here. | `fn` | Tool | Does | |---|---|---| | `brain.list` | `selda_list_brain` | read every item, plus the allowed types | | `brain.add` | `selda_add_brain_item` | add one item | | `brain.update` | `selda_update_brain_item` | rewrite one item's title and/or body | | `brain.remove` | `selda_remove_brain_item` | take one item back out | All four are free on a test key. ### Item types Exactly six, and a value outside them is refused rather than stored. An item the app cannot render is an item nobody can see, edit or delete, which is worse than a rejected call. | `type` | For | |---|---| | `company` | a fact about the business itself | | `product` | something it sells | | `partner` | a partner or reseller | | `reference` | proof: a named customer, a result, a case | | `note` | anything else worth knowing | | `avoid` | a hard rule Selda must never break, e.g. "never claim we are the cheapest" | ### Example ```jsonc // POST /mcp/mutate { "fn": "brain.add", "args": { "projectId": "<workspace id>", "type": "reference", "title": "Summer campaign 2026", "body": "Ran this campaign for six weeks and booked 14 meetings from it." }} // -> { "value": { "ok": true, "id": "m2x8k1-3-a9f2", "count": 7 }, "request_id": "req_..." } ``` ```json // POST /mcp/query { "fn": "brain.list", "args": { "projectId": "<workspace id>" } } { "value": { "types": ["company", "product", "partner", "reference", "avoid", "note"], "items": [ { "id": "m2x8k1-3-a9f2", "type": "reference", "title": "Summer campaign 2026", "body": "Ran this campaign for six weeks...", "createdAt": 1785321674000 } ] }, "request_id": "req_9f2c1a4b7e0d3a6f8c2b1e05" } ``` `brain.update` and `brain.remove` answer `{ "ok": false, "reason": "not_found", "id": "..." }` when that id is not in the workspace, rather than reporting a success that changed nothing. **What you write here may be quoted verbatim in a message to a real customer.** Keep it factual. An invented reference in the Brain becomes a lie in an email. --- ## Inbound: telling Selda something happened Every other lead in Selda starts from Selda's own search. `events.ingest` is the other direction: your website, your form, your ad landing page, your own tooling reports that **something happened**, and Selda works out what that means. One call. Not "add the lead, then write the note, then attach it to a campaign" with your code holding the order straight. - new person, so a lead is created - somebody Selda already knows, so the event lands on their existing lead - an address that asked Selda to stop, so nothing is created and you are told why MCP tool: `selda_ingest_event`. HTTP: `POST /mcp/mutate` with `fn: "events.ingest"`. Requires the **Selda Inbound** add on on a live key. Free on a test key, so you can build the whole integration before you buy anything. ### `autoAdvance`: the answer is written before anyone looks Without it, an arriving lead is stored and nothing else happens: `status: new`, no messages. The lead is safe and somebody still has to notice it, open it and start from a blank box. Pass `autoAdvance: true` and Selda reads the workspace's **Brain**, its price list, its offer rules, its tone, and the things it has forbidden itself to say, writes the reply in the language the enquiry was written in, and leaves it in the Sales Inbox. Then `draft.ready` fires, so you find out because you were told rather than because you checked. It is off by default because it spends credits, and **it never sends**. The draft waits for the same human approval as every other draft in Selda. If the workspace has no Brain material to answer from, nothing is drafted and `draft.failed` says `no_brain_material`. That is deliberate: a quote answered from an empty Brain is an invented price, and an invented price approved by somebody who assumed Selda knew the real one is worse than no draft at all. ### Request ```jsonc { "fn": "events.ingest", "args": { "projectId": "<workspace id>", // required "type": "form_submitted", // required. Your own wording. "identity": { // required. At least one of email / linkedinUrl / a name. "email": "matti@example.fi", "domain": "example.fi", // context only, never used to decide identity "linkedinUrl": "https://www.linkedin.com/in/matti/", "name": "Matti Meikalainen", // or firstName / lastName separately "company": "Example Oy", "jobTitle": "CTO", "phone": "+358 40 1234567" }, "payload": { "form": "analysis", "findings": ["Languages: missing"], "price": 290 }, "source": "example.com", "occurredAt": "2026-08-06T10:21:14.152Z", // ISO 8601 or epoch ms. Defaults to now. "idempotencyKey": "report-example.fi-2026-08-06", "attachToRunId": "<campaign run id>", // optional "analysis": "Full research write-up...", // optional, grounds the eventual message "tags": ["inbound", "analysis"] } } ``` ### Response ```json { "value": { "leadId": "k57f...", "created": true, "matchedExisting": false, "matchedBy": null, "blocked": false, "reason": null, "eventId": "j92a...", "replayed": false, "attached": { "ok": true, "runId": "m31c..." }, "identityScanTruncated": false, "occurredAtFallback": false, "message": "Lead created and waiting for review. Nothing has been sent." }, "request_id": "req_9f2c1a4b7e0d3a6f8c2b1e05" } ``` Every field is always present, so you never have to decide whether a missing key means `false` or means the field does not exist. | Field | Meaning | |---|---| | `leadId` | the lead this event is about. `null` only when `blocked` | | `created` · `matchedExisting` | exactly one is true, unless `blocked` | | `matchedBy` | `"email"`, `"linkedin"`, or `null` | | `blocked` · `reason` | `true` with `suppressed_email` / `suppressed_domain` when the address asked Selda to stop | | `eventId` | the row on the lead's timeline | | `replayed` | `true` when this `idempotencyKey` was already seen. Nothing changed | | `attached` | `{ ok: true, runId }`, or `{ ok: false, reason }` with `not_requested`, `run_not_found`, `run_in_another_workspace`, `already_on_run`, `blocked` | | `identityScanTruncated` | the LinkedIn only match hit its 1000 lead ceiling, so a match may have been missed. Said out loud rather than passed off as "no match" | | `occurredAtFallback` | your `occurredAt` was unusable or out of range, so the receive time was used | ### Identity is a person, never a company Matching is on the **email address**, and failing that the **LinkedIn profile**. Never the domain. Two people at the same employer share a domain, and merging them would attach one person's history to the other with no way to tell afterwards. So `domain` is stored as company context and is never allowed to decide who someone is. A near miss creates a new lead you can merge by hand, which is recoverable. A wrong merge is not. An event carrying only a `domain` is refused, because it names an employer and not a person. ### Retries are safe Pass an `idempotencyKey` and a repeat returns the first result with `replayed: true`, creating nothing. This is what lets you call Selda from a webhook handler that has no way of knowing whether its first attempt landed. ### Joining a campaign `attachToRunId` puts the lead on an existing campaign's review list. The campaign already has a tone, a signature, a follow up rhythm and safety rules that somebody approved; an inbound lead joining that beats it inventing a process of its own. **It joins at review, with no message written.** A launch selects drafts that are written, approved or queued. A row created this way is none of those, so no launch can pick it up and a human sees it first, exactly like a discovered lead. ### Errors specific to this call | Condition | Result | |---|---| | `identity` names no person | 500 `function_error`, message says what to send instead | | `projectId` in another organization | 500 `function_error`, "Project not found or unauthorized" | | address or domain suppressed | **200** with `blocked: true`. Not an error: the event is recorded, no lead is made | | workspace lacks the add on (live key) | 403 `addon_required` | --- ## Flows: what happens when something arrives `events.ingest` records what happened and can draft a reply. A **flow** is the layer above it: the workspace has said, once, what should happen to an arrival like this one, and the same definition is readable and writable from here and from the app. A flow is a trigger plus ordered steps. ```json { "name": "Website enquiries", "trigger": { "kind": "inbound_event", "eventTypes": ["widget_enquiry"], "sources": ["example.com"] }, "steps": [ { "id": "s1", "kind": "understand", "skillId": "<from flows.saveSkill>", "categories": [ { "key": "video", "label": "Video production", "hint": "shoots, edits, event coverage" }, { "key": "seo", "label": "Site and SEO", "hint": "visibility, rebuilds, bookings" } ] }, { "id": "s2", "kind": "route", "routes": [ { "categoryKey": "video", "runId": "<campaign run id>" }, { "categoryKey": "seo", "inboxOnly": true } ] }, { "id": "s3", "kind": "draft_reply" }, { "id": "s4", "kind": "notify", "emails": ["you@company.com"], "webhook": true } ] } ``` An empty `eventTypes` or `sources` means **any**. `trigger.kind` may also be `manual`, for a flow you only run by hand while you are writing it. ### The steps, and the line they cannot cross | `kind` | Does | Never | |---|---|---| | `understand` | reads the enquiry against your instruction file and the Brain, and picks ONE of **your** categories | invents a category. No fit answers `unknown`, with a reason | | `route` | sends each category onto a campaign run, or onto none (`inboxOnly`) | routes a category you have not decided about. It says so in the log instead | | `attach` | puts the lead on one campaign run's review list whatever the category | writes a message there | | `draft_reply` | writes the answer into the Sales Inbox, where it waits for approval | sends | | `notify` | emails people, and fires this workspace's webhooks as `flow.completed` | reaches the enquirer | **There is no sending step and there will not be one.** `launchRun` is refused by path for every key, and a flow's furthest act is a draft plus a notification. That is the same gate a person meets in the app, not a weaker one for automation. ### When a flow and `autoAdvance` both write If a flow matching the event has a `draft_reply` step, it owns the drafting and `events.ingest`'s own `autoAdvance` stands down. The response says so (`autoDraft.reason: "flow_writes_it"`) and lists the flows the event set off, so nothing has to be inferred from an inbox that looks emptier than expected. ### Reading what it did `flows.runs` returns every step of every run with a `status` of `ok`, `skipped` or `failed` and a summary in plain words. A skip is a row with its reason, never an absence. That is the thing to read when a flow behaves unexpectedly, rather than re-reading its definition. --- ## Test keys and live keys Two kinds of key, and the difference is not "test data versus real data". It is **what Selda is allowed to spend on your behalf.** - `sk_test_…` **test key.** Free on every plan, including Free. Read everything in the workspace, write your own data in, build and test a whole integration. - `sk_live_…` **live key.** Requires a paid plan (Pro and up). Adds the parts that cost money: finding people, enriching them, running discovery, and reaching a real recipient. ### What a test key can do Everything you need to build against Selda without paying: - every read: `projects.*`, `leads.list` / `leads.get`, `campaigns.*`, `runs.status`, `messages.*`, `knowledge.get`, `brain.list`, `credits.info`, `connectors.list`, `webhooks.list` - every write of **your own** data: `leads.add` / `addBatch` / `update` / `addTag` / `merge` / `delete`, `knowledge.set` / `append`, `brain.add` / `update` / `remove`, `projects.updateContext`, `campaigns.*` (the legacy table), `messages.approve`, `webhooks.*`, `connectors.create` / `delete` - `POST /mcp/material/upload` and `material.import` **without** `autoAdvance` - `events.ingest`, so you can build and test an inbound integration end to end - `messages.generate`, `replies.draft`, `replies.classify`: drafting against the companies you already have is the point of test mode ### What needs a live key on a paid plan | `fn` | Why | |---|---| | `company.lookup` | resolves real people's contact details through a paid provider, billed per call | | `leads.enrich` · `leads.enrichBatch` | same, per record | | `engine.start` | runs discovery: web search, crawls, per lead credit spend | | `material.import` **with** `autoAdvance` | the grant carries the run into contact lookup and drafting | | `connectors.sync` | pulls and enriches from an outside source | **There is no send function, on any key.** This table used to list `messages.send` as something a live key unlocks. No such function exists in the registry and none is planned: sending happens only in the Selda app, after a human approves each message. A live key buys the calls above, which spend money or reach a provider, never the one that reaches a recipient. `GET /mcp/capabilities` has always said so in its own note; this page did not. A test key calling one of these gets a `403` whose `message` is a sentence explaining what the call does, why it needs a live key, and what your key can still do. The machine readable form is in `code`: ```json { "error": { "type": "authorization_error", "code": "live_key_required:paid_contact_data", "message": "This looks up real people's contact details through paid data providers, so it needs a live key on a paid plan. Your test key can still add companies you already have, import your own material, and draft against them.", "request_id": "req_9f2c1a4b7e0d3a6f8c2b1e05" }, "request_id": "req_9f2c1a4b7e0d3a6f8c2b1e05" } ``` The reasons you can get back: `paid_contact_data`, `paid_engine_run`, `external_sync`, `workspace_creation`, `sending`, `admin_only`. Each capability in `GET /mcp/capabilities` carries `sandboxAllowed` and `liveOnlyReason`, so you can read the whole line off the manifest instead of discovering it one `403` at a time. --- ## Errors Every failure has the same shape, on every endpoint: ```json { "error": { "type": "invalid_request_error", "code": "unknown_fn", "message": "Unknown query: leads.lst. Available: projects.list, projects.get, …", "request_id": "req_9f2c1a4b7e0d3a6f8c2b1e05" }, "request_id": "req_9f2c1a4b7e0d3a6f8c2b1e05" } ``` `request_id` also comes back in the `X-Request-Id` response header, so you can log it without parsing the body. It contains nothing secret. Quote it in a support message. | Status | `type` | `code` | What happened | |---|---|---|---| | 400 | `invalid_request_error` | `invalid_body` | body was not JSON, or not `{ fn, args }` | | 400 | `invalid_request_error` | `missing_fn` | no `fn` in the body | | 400 | `invalid_request_error` | `unknown_fn` | no such `fn`. The message lists the valid ones | | 400 | `invalid_request_error` | `missing_path` | `/mcp/material/upload` without `X-Selda-Path` | | 400 | `invalid_request_error` | `empty_file` | upload body was empty | | 401 | `authentication_error` | `invalid_api_key` | key is wrong, revoked, or expired | | 403 | `authorization_error` | `missing_scope` | key lacks `read` / `write` / `pipeline` / `admin` | | 403 | `authorization_error` | `plan_not_eligible` | live key whose org is no longer on a paid plan | | 403 | `authorization_error` | `live_key_required:<reason>` | test key on something that spends money | | 403 | `authorization_error` | `addon_required` | the workspace does not hold a required add on | | 413 | `invalid_request_error` | `file_too_large` | upload over 25 MB | | 429 | `rate_limit_error` | `rate_limit_exceeded` | over 100 requests in the current minute. `Retry-After: 60` | | 500 | `api_error` | `function_error` | the function itself refused, with a message meant for you | | 500 | `api_error` | `internal_error` | something broke on our side. Send us the `request_id` | An `invalid_api_key` and a `projectId` outside your organization deliberately look the same as "no such thing": a key must not be able to probe what exists in someone else's workspace. --- ## Limits | Limit | Value | |---|---| | Requests per key | 100 per minute, sliding window. `X-RateLimit-Limit` and `X-RateLimit-Remaining` on every response | | Upload size | 25 MB per file (`/mcp/material/upload`) | | Leads per `leads.addBatch` | 500 | | `company.lookup` results | 15 maximum, 6 by default | | `idempotencyKey` length | 200 characters. Longer is treated as no key at all | | LinkedIn only identity match | scans 1000 leads. If it hits that ceiling, the response says `identityScanTruncated: true` rather than quietly missing a match | Nothing here expires a key on its own. Keys live until you revoke them. ## What things cost **There is no per tool price list, and publishing one would be a guess.** Selda charges from *observed* spend: every LLM call and paid vendor call reports its real cost, the run accumulates it, and credits are that spend times a fixed margin. A discovery run over ten hard to research companies genuinely costs more than one over ten easy ones, and a price table would have to lie about that in one direction or the other. What is true and worth planning around: - **Reads are free.** No query on `/mcp/query` costs a credit. Neither does receiving a reply, registering a webhook, or anything in the Brain. - **Writing your own data in is free.** `leads.add`, `material.upload`, `material.import` without `autoAdvance`, `knowledge.*`, `brain.*`. You are not charged for handing Selda what you already had. - **Work Selda does costs.** Discovery, research, enrichment, message writing, sending. That is `engine.start`, `company.lookup`, `leads.enrich*`, `messages.generate`, `material.import` with `autoAdvance`, and `connectors.sync`. - **Selda's own failures are never charged.** A run that errors and retries is our cost. - Test-mode work is charged at the same margin as paid work. The free tier's gift is the SIZE of its grant, 30 credits, about one full run, not a discounted rate, so what a credit buys is the same number wherever you are. Call `credits.info` before and after a run to see the actual number for your workload. That is the only honest way to size it, and it is one call. --- ## Tools Note what is **not** here: earlier versions of this server had `selda_create_campaign` and `selda_add_leads_by_tag`, which wrote to a legacy `campaigns` table the campaign-flow UI does not read, so a campaign created that way was invisible in the app. They were removed. To create a real, reviewable campaign, use `selda_upload_material` + `selda_import_material` below: it creates one via the same path the browser's folder-drop uses. The table below is **generated from the servers themselves**. It carried a hand-written list of 22 tools until 3.9.2026, while the hosted server had 62 and the sentence above it said both servers exposed the same set. Both numbers and the claim were wrong, and nothing was checking. Now a tool added to a server is a row on this page or a failing test. {/* GENERATED:mcp-tools. Do not edit by hand. Source: api/mcp.ts + mcp-server/index.ts Regenerate: node scripts/generate-mcp-tools.mjs convex/lib/__tests__/mcpToolsAreGenerated.test.ts fails if this block drifts. */} The hosted server serves **62 tools**. The local stdio server serves **32**, a subset: 32 of the hosted tools are on both, and 28 are hosted only. They are not interchangeable, so pick the hosted one unless you specifically need a local process. ### The two tools ChatGPT looks for OpenAI's connector contract does not read tool descriptions to decide what a server can do. It requires two tools by NAME, with fixed argument and result shapes, and a server without them connects and then has nothing ChatGPT will call. Selda serves both. They are a read-only view over the tools below, not a second data path, and every id `search` returns is one that `fetch` accepts. | Tool | Purpose | Arguments | |------|---------|-----------| | `search` | Search this Selda workspace: leads (people and companies), projects, and Brain items (the facts a human told Selda about the business). Returns matches with an id you can pass to `fetch` for the full record. Use this first when the user asks about a person, a company, or what Selda knows. | `query`, `projectId?` | | `fetch` | Fetch the full record behind an id returned by `search`. Ids look like `lead:…`, `project:…` or `brain:…`. Returns the record as readable text. | `id` | ```json // search({ query: "nordic books" }) { "results": [ { "id": "lead:k57...", "title": "Mika Virtanen · Nordic Books Oy", "url": "https://app.selda.ai/workspace-..." } ] } // fetch({ id: "lead:k57..." }) { "id": "lead:k57...", "title": "Mika Virtanen · Nordic Books Oy", "text": "Name: ...\nResearch: ...", "url": "...", "metadata": { "kind": "lead", "draftWaitingForApproval": true } } ``` Ids are `lead:…`, `project:…` and `brain:…`. An id of any other shape is a tool error, not an empty result, so a client cannot quietly show nothing. ### Everything else Arguments marked `?` are optional. A tool marked `*` is on the hosted server only. | Tool | Purpose | Arguments | |------|---------|-----------| | `selda_list_projects` | List all your Selda projects. Each project represents a product/company you're doing GTM for. Returns project name, website, status, and ID that you'll need for other tools. | none | | `selda_get_project` | Get detailed info about a project: business context, market analysis, ICP, and settings. | `projectId` | | `selda_list_leads` | List leads (potential customers) for a project. Shows contact info, company, fit score, and outreach status. | `projectId`, `limit?` | | `selda_get_lead` | Get full details about a lead: contact info, company research, fit analysis, why they're a good lead, outreach angle, and message history. Returns `analysisStored` and `analysis` so you can VERIFY that research you sent with selda_add_lead actually landed, a lead added that way belongs to no run, so selda_get_run_leads cannot see it. Also returns any draft written for it. | `leadId` | | `selda_update_lead` | Correct a lead. Notes append by default; status, email, name, title, domain and the analysis are replaced. `analysis` is the same text you passed to selda_add_lead and it is what Selda writes the message FROM, if it contains a wrong claim, fix it here before starting a campaign, because the message will carry it. | `leadId`, `notes?`, `appendNote?`, `status?`, `analysis?`, `email?`, `firstName?`, `lastName?`, `jobTitle?`, `companyDomain?`, `outreachAngle?`, `dealStage?`, `dealValueEur?` | | `selda_list_campaigns` | List campaigns for a project. Campaigns contain leads, messages, and execution status. | `projectId` | | `selda_get_campaign` | Get campaign details: status, leads, messages, channels, performance stats. | `campaignId` | | `selda_campaign_stats` | Get campaign performance numbers: how many sent, delivered, opened, clicked, replied, bounced. | `campaignId` | | `selda_list_messages` | List sent/received messages for a project, newest first. Paged: pass `offset` from the returned `nextOffset` to read further back. Bodies are shortened here and `bodyTruncated` says which ones were, use selda_get_thread to read one conversation in full. | `projectId`, `limit?`, `offset?`, `since?`, `direction?` | | `selda_get_thread` | Get the full email thread with a specific lead, all sent and received messages. | `leadId` | | `selda_run_pipeline` | Run Selda's AI engine to find leads by searching the web. Give it a description of WHO to find, e.g. 'B2B SaaS founders in Finland' or 'AI startups that recently raised seed funding'. Do NOT pass URLs here, the engine searches the web itself. If the user provides a URL/webpage with companies, use selda_add_lead to add them directly instead. | `projectId`, `idea`, `targetLeadCount?` | | `selda_add_lead *` | Add a lead to a Selda project. PREFERRED method when the user shares a URL, webpage, list, or specific companies. Read the page yourself, extract the companies, and call this tool for each one. Much more reliable than run_pipeline for URL-based prospecting. Call multiple times to add multiple leads. If you have researched or analyzed the company, pass that write-up in `analysis`, Selda uses it as the authoritative research grounding: it writes the outreach FROM your analysis (skipping its own crawl) and labels the message 'Based on your analysis'. | `projectId`, `company`, `firstName?`, `lastName?`, `email?`, `jobTitle?`, `companyDomain?`, `linkedinUrl?`, `phone?`, `notes?`, `analysis?`, `outreachAngle?`, `whyGoodLead?`, `source?` | | `selda_list_brain` | Read the workspace's Brain: the structured things a human told Selda about this business - products, partners, references and proof, company facts, notes, and the hard rules Selda must never break. This is what every message is written from, so read it before writing anything, and add to it rather than guessing. Returns the allowed item types alongside the items. | `projectId` | | `selda_add_brain_item` | Add ONE thing Selda should know about this business. Use this to fill a new workspace and to grow it as you learn more. `type` must be one of: company (a fact about the business), product (something it sells), partner, reference (proof, a named customer, a result), note, or avoid (a hard rule Selda must never break, e.g. 'never claim we are the cheapest'). Keep `title` short and `body` factual: what you write here may be quoted in a real message, so anything invented here becomes a lie to a customer. | `projectId`, `type`, `title`, `body` | | `selda_update_brain_item` | Rewrite the title and/or body of one Brain item. Pass the id from selda_list_brain. Returns ok:false with reason 'not_found' if that id is not in this workspace, rather than silently doing nothing. | `projectId`, `id`, `title?`, `body?` | | `selda_remove_brain_item` | Remove one Brain item. The human owns what Selda knows, so anything written here can be taken back out. | `projectId`, `id` | | `selda_ingest_event` | Report that something happened outside Selda: someone filled in a form, finished an analysis, answered an ad, signed a contract. ONE call does the whole thing, Selda creates the lead if the person is new, recognises them if they are already known, records the event on their timeline, and can put them on an existing campaign's review list. Use this instead of selda_add_lead whenever the lead ARRIVED (inbound) rather than being found by a search. Identity is matched on email address or LinkedIn profile, never on the company domain, so two people at the same employer stay two people. Pass idempotencyKey and you can safely retry after a network failure. Pass autoAdvance to have Selda write the reply straight away from the workspace's Brain (its price list, offer rules, tone and the things it must never promise) and leave it in the Sales Inbox, the draft.ready webhook fires when it is there, so nobody has to poll. This never sends anything: the lead lands in review and a human presses send in the Selda app. | `projectId`, `type`, `identity`, `payload?`, `source?`, `occurredAt?`, `idempotencyKey?`, `attachToRunId?`, `analysis?`, `tags?`, `autoAdvance?` | | `selda_credits` | Check your Selda credit balance: daily free credits, lead credits, usage. | none | | `selda_lookup` | Look up a company by name or website and get its decision-makers WITHOUT starting a campaign. Returns enriched company info and the right people to reach, seniority matched to company size. Use for quick research like 'who runs growth at Acme?'. | `company`, `role?`, `limit?` | | `selda_list_runs *` | Every campaign run in a project, newest first, with status and phase. Use this when you no longer have a runId, for example when a call's response did not reach you. Read-only. | `projectId`, `limit?` | | `selda_get_run_status` | Where a run is right now. Returns companiesFound, leadsWithContact, leadsHeldForContact (waiting for a person to decide something), leadsWithoutDraft (no message written) and droppedCompanies (companies that were on the run and are not rows any more, each with the reason). Read leadsWithoutDraft and droppedCompanies before concluding a run is finished: awaiting_approval means the run stopped for a human, not that every company got a message. | `runId` | | `selda_start_campaign_from_leads *` | Start a campaign from leads you already pushed in with selda_add_lead, selected by the `source` label you gave them. This is the loop: you research and write the analysis, push each company with selda_add_lead { source: 'my-batch-name', analysis: '...' }, then call this once. Selda writes a message per lead FROM the `analysis` you pushed with it, and skips its own crawl for every lead that has one. A lead you pushed WITHOUT an analysis is researched by Selda instead, because there is nothing else to write from. It stops at the drafts. It SPENDS CREDITS and it SENDS NOTHING, approving and sending are a human press in the app. | `projectId`, `source`, `campaignId?`, `campaignBrief?` | | `selda_confirm_companies *` | Confirm a run's company list so Selda goes on to find the decision-makers and draft the messages. This is the gate selda_get_run_status reports as waiting on the app, it is now reachable here, so a campaign can be built end to end from your own tool. It SPENDS CREDITS (research + drafting) and it SENDS NOTHING: the send is still a human press in the Selda app, and no tool here will ever send. Pass the language and channel the campaign should use; everything else is optional and Selda keeps what it already proposed. | `runId`, `language`, `channel`, `targetRoles?`, `targetMarket?`, `leadsPerCompany?`, `excludedDomains?` | | `selda_get_run_leads` | Read what a run actually produced: every company it found, the contact it resolved for each, and the message Selda drafted for them. This is how you review or improve drafts from your own tool, selda_get_run_status only returns counts. Nothing here sends; sending happens in the Selda app. | `runId`, `limit?`, `withDraftOnly?` | | `selda_draft_reply` | Write a reply DRAFT into a lead's Sales Inbox thread. The draft prefills the thread's composer in the Selda app, where a person reviews and sends it. This tool can send nothing. Use the leadId from selda_list_leads / selda_get_thread. | `leadId`, `body` | | `selda_add_inbox_message` | Put one message into a lead's Sales Inbox thread, in either direction, even for somebody who was never in a campaign. direction 'inbound' = something THEY wrote; 'outbound' = something already sent from elsewhere, recorded as history. Pass `identity` instead of `leadId` to create the person. This tool sends nothing. | `projectId`, `direction`, `body`, `leadId?`, `identity?`, `subject?`, `occurredAt?` | | `selda_add_lead_alias` | Claim another email address for a lead (they replied from a different mailbox). Future inbound from that address lands in the same conversation, and earlier unlinked inbound from it is adopted onto the lead. | `leadId`, `email` | | `selda_update_draft` | Rewrite the subject or body of one drafted message, using the leadId from selda_get_run_leads. Use this after improving a draft in your own tool. A message that has already been sent is a record and cannot be edited, and editing never approves or sends anything. | `leadId`, `subject?`, `body?` | | `selda_remove_draft` | Take one drafted message out of a run so it cannot be sent, using the leadId from selda_get_run_leads. Use this when a draft is wrong enough that rewriting it is not the answer. Nothing is deleted: the row stays visible with your reason and a person can put it back in the app. A message that already went out is a record and is refused. | `leadId`, `reason?` | | `selda_archive_run` | Close a campaign run and take it off the active list, e.g. a run that found nothing and sits in awaiting_approval that nobody can approve. Keeps every contact and every message; deleting contacts is a separate decision a person makes in the app. | `runId` | | `selda_rename_campaign` | Name a campaign run so a person can tell it apart in the list. Without this the title is derived from the brief's first line, which makes batches indistinguishable. An empty name restores the derived title. | `runId`, `name` | | `selda_get_message_structure` | Read what a campaign's message is made of: every block in order, which ones ship word for word, the instruction behind each block Selda writes, the paragraph count, and whether the structure is locked. Use it before changing anything. | `campaignId` | | `selda_set_message_structure` | Say what you want a campaign's message to be. Blocks with kind 'locked' ship WORD FOR WORD and nothing rewrites them; blocks with kind 'generated' carry your instruction and Selda writes that part from real material. Also takes the paragraph count and what must never appear. A locked structure is refused until you unlock it. This changes how messages are written; it sends nothing and approves nothing. | `campaignId`, `objective?`, `blocks?`, `form?`, `must?`, `mustNever?`, `followUpStrategy?` | | `selda_lock_message_structure` | Lock a campaign's message structure so every locked block ships exactly as written and nothing rewrites it. Pass locked: false to take it back to draft; a lock you cannot undo is Selda refusing you your own document. Sends nothing. | `campaignId`, `locked` | | `selda_delete_lead *` | Delete one lead. Selda never removes a lead on its own, this is the caller's act, and it is how a trial install clears its own test rows. | `leadId` | | `selda_delete_leads *` | Delete several leads at once, by id. Same act as selda_delete_lead, for clearing a batch. | `leadIds` | | `selda_merge_leads *` | Merge a duplicate lead into the one you are keeping. The duplicate's addresses, timeline and messages move onto the primary, so the same person stops appearing twice across campaigns. | `primaryId`, `duplicateId` | | `selda_create_webhook *` | Register an endpoint Selda POSTs to when something happens, so you are told rather than having to poll. Events: lead.added, reply.received, message.sent, lead.status_changed, campaign.completed, credits.low, meeting.booked, test.ping. Deliveries are signed and retried; ten consecutive failures disable the endpoint. | `url`, `events`, `description?` | | `selda_list_webhooks *` | Every registered webhook for this workspace, with its events and whether delivery has been auto-disabled. | none | | `selda_delete_webhook *` | Remove a registered webhook. | `webhookId` | | `selda_list_flows *` | The workspace's FLOWS: what Selda does when something arrives from outside (a website widget, a form, anything reported through selda_ingest_event). Each flow is a trigger plus steps in order. Read this before writing one, so you extend what exists instead of adding a second flow that answers the same events. Also returns the workspace's flow instruction files. A flow can never send: its furthest step leaves a draft in the Sales Inbox and emails a person. | `projectId` | | `selda_flow_runs *` | What a flow actually did, newest first: every step, whether it worked, was skipped or failed, and why in plain words. Read this to debug a flow rather than guessing from its definition. | `flowId`, `limit?` | | `selda_create_flow *` | Create a flow. `trigger.kind` is 'inbound_event' (anything arriving through selda_ingest_event, including the website widget) or 'manual'. Narrow it with `eventTypes` and `sources`, or leave them out to answer everything. `steps` run in order and may be: understand (place the enquiry in one of YOUR categories, using an instruction file and the Brain), route (send each category onto a campaign run or onto none), attach (onto one campaign run whatever the category), draft_reply (write the answer into the Sales Inbox, where it waits for human approval), notify (email people that it happened). NOTHING SENDS: there is no step that reaches the enquirer. Created switched OFF unless you pass enabled:true - read the steps first. | `projectId`, `name`, `trigger`, `steps`, `enabled?` | | `selda_update_flow *` | Rewrite a flow's name, trigger or steps. Pass only what changes; steps REPLACE the whole list, so read selda_list_flows first and send the full ordered set. | `flowId`, `name?`, `trigger?`, `steps?` | | `selda_set_flow_enabled *` | Switch a flow on or off. Off means matching events still create the lead and the timeline row as before; only this flow's steps stop running. | `flowId`, `enabled` | | `selda_delete_flow *` | Delete a flow and its run log. The leads and campaigns it touched are untouched. | `flowId` | | `selda_save_flow_skill *` | Write or rewrite an instruction file that an 'understand' step reads: how a person at this company decides which category an enquiry belongs to. Plain language, in the operator's own words. Pass skillId to rewrite an existing one. This is material, not code: it is read alongside the Brain, and anything factual in it may end up shaping a real reply. | `projectId`, `name`, `body`, `skillId?` | | `selda_get_knowledge *` | The workspace's knowledge notes: the prose that grounds every message Selda writes. Read it before changing it. | `projectId` | | `selda_set_knowledge *` | REPLACE the workspace's knowledge notes. Overwrites what is there, read it first with selda_get_knowledge unless you mean to discard it. For structured facts (products, references, things never to say) use selda_add_brain_item instead. | `projectId`, `text` | | `selda_append_knowledge *` | Add to the workspace's knowledge notes without touching what is already there. | `projectId`, `text` | | `selda_add_leads *` | Add several known companies or contacts at once. Same as selda_add_lead per row, including the `analysis` field that grounds the message on research you already did. | `projectId`, `leads` | | `selda_tag_lead *` | Tag a lead. Tags are how selda_start_campaign_from_leads selects which leads a campaign runs on. | `leadId`, `tags` | | `selda_approve_message *` | Mark a drafted message approved. Approval is not sending: the send is still a human press in the Selda app, and nothing here reaches anybody. | `messageId` | | `selda_generate_message *` | Draft a message for one lead. Leaves it as a draft; it sends nothing. | `leadId`, `outreachAngle?`, `channel?` | | `selda_classify_replies *` | Read the workspace's unclassified inbound replies and label each one (interested, not now, wrong person, unsubscribe…). Changes no drafts and sends nothing. | `projectId` | | `selda_update_project_context *` | Rewrite what Selda understands about the business: what they do, the industry, the value proposition, the products. This is what every message is written from, so a change here changes every future message. | `projectId`, `whatTheyDo?`, `industry?`, `valueProposition?`, `products?` | | `selda_list_connectors *` | Data connectors active on this workspace, and their sync state. | `projectId` | | `selda_preview_reply *` | Ask how Selda would answer an enquiry, using this workspace's Brain, WITHOUT creating a lead or storing anything. Use it to tune the Brain: send an example form, read the reply, adjust the price list or the rules with selda_add_brain_item, try again. It runs the same writer the real reply uses, so what you see here is what a real enquiry gets. Stores nothing, sends nothing, leaves no rows to clean up. | `projectId`, `eventType`, `payload?`, `who?`, `company?`, `source?`, `language?` | | `selda_upload_material` | Upload one file of prospect material (PDF, Markdown, JSON, plain text) so a campaign can use it as its authoritative source instead of a fresh crawl, e.g. an analysis you already wrote about that prospect. Call once per file, then hand ALL the returned file objects to selda_import_material together. The `path` also tells Selda which company the file belongs to, use a folder-per-company layout, e.g. 'boreo/analyysi.pdf', 'boreo/tiedot.md'. FOR A PDF OR ANY LARGE FILE, PASS `url` INSTEAD OF `content`. Selda fetches it server-side and the bytes never travel through this conversation. Base64 in a tool call is impractical for real documents: a 1.6 MB PDF is about 2.2 million characters. Text you already hold goes in `content` with encoding 'utf8'. | `projectId`, `path`, `url?`, `content?`, `encoding?`, `mimeType?` | | `selda_import_material` | Turn uploaded material into a campaign + company list. Pass every file object returned by selda_upload_material. By default this ONLY builds the company list and stops there for human review, it does not research decision-makers or write messages, and it can never send. To let it continue automatically, pass autoAdvance: ['leads'] (find decision-makers), ['leads','messages'] or true (also draft messages), or ['messages'] if leads are already known. Drafted messages still require human approval in the Selda app before anything sends. | `projectId`, `files`, `campaignName?`, `targetRunId?`, `campaignBrief?`, `autoAdvance?` | `*` marks a tool the hosted server has and the local stdio server does not. {/* /GENERATED:mcp-tools */} ### Usage guidance (from the MCP server instructions) - The app is at `https://app.selda.ai`, never use `selda.city` or other domains. - **Always call `selda_list_projects` first** to obtain a `projectId` before any other operation. - When a user shares a URL or webpage with specific companies/people, **add them directly** (e.g. `selda_add_lead`), do **not** pass URLs to `selda_run_pipeline`. - Use `selda_run_pipeline` only for **open-ended searches** like "find SaaS founders in Finland". - `selda_run_pipeline` runs the engine and **costs credits**. It stops at the company list and waits for a person in the app; poll `selda_get_run_status` and read `awaitingHuman` before saying anything about whether it finished. --- ## Connecting a client **OAuth (recommended for Claude.ai, ChatGPT, Cursor):** add `https://mcp.selda.ai/api/mcp` as a connector. The server advertises `WWW-Authenticate: Bearer resource_metadata=…` on an unauthenticated request, pointing at `GET /.well-known/oauth-protected-resource`, which in turn points at the authorization server's `GET /.well-known/oauth-authorization-server` (`https://api.selda.ai`). A client that supports this discovers the whole flow automatically: it self-registers via `POST /oauth/register` (RFC 7591, no human step), sends the user's browser to `authorization_endpoint` (`https://app.selda.ai/oauth/authorize`) where they log in and approve a workspace, then exchanges the resulting code for an access token via `POST /oauth/token` (Authorization Code + PKCE, `code_challenge_method=S256`). The access token **is** a regular `sk_live_…` key under the hood, same org-scoping, same revoke button in **Settings → Apps → Selda MCP**. OAuth is just a way to mint it without copying anything by hand. **Static key (Claude Code, scripts, CI):** 1. In the Selda app go to **Settings → Apps → Selda MCP** and create a key. Copy the `sk_live_…` value (shown once). 2. ```bash claude mcp add --transport http selda https://mcp.selda.ai/api/mcp \ --header "Authorization: Bearer sk_live_xxx" ``` Or the equivalent JSON block for any other client that reads MCP config (Claude Desktop, Cursor), same URL, same header, no local install. 3. Every call is automatically scoped to the org that owns the key. Pipeline runs consume credits, so check `selda_credits` if runs start failing. **Local stdio** (`mcp-server/index.ts`) still exists for local development, spawned by a client via `npx tsx` with `SELDA_API_KEY` in its environment, but the hosted server above is the current recommended path for every client that supports remote MCP. --- ## Changelog Tool arguments and response shapes change here first. A dated entry means the behaviour is live. ### 2026-08-06 **Added: `selda_ingest_event` / `events.ingest`.** One call for an inbound lead: creates the person if new, recognises them if known, records the event on their timeline, optionally joins a campaign. Identity matches on email or LinkedIn profile, never on the company domain. Supports `idempotencyKey` for safe retries. Requires the Selda Inbound add on on a live key; free on a test key. It cannot send. **Added: the Brain over MCP.** `brain.list`, `brain.add`, `brain.update`, `brain.remove` and their `selda_*_brain_item` tools. The structured knowledge a workspace is built from was previously readable only in the app. **Changed: `selda_update_lead` appends notes instead of replacing them.** Its description always said "notes to add" and it overwrote. Pass `appendNote: false` for the old behaviour. The raw `leads.update` fn still replaces by default, so a direct HTTP integration is unaffected; opt in with `appendNote: true`. **Changed: `leads.add` refuses a suppressed address.** It returns `blocked: true` with a reason and creates nothing, instead of creating a lead that may never be contacted. Its response now always carries `leadId`, `duplicate`, `blocked` and `reason`, so the shape no longer varies by path. **Changed: test keys are no longer allowed to spend.** A test key can read the whole workspace and write your own data in, but `company.lookup`, `leads.enrich*`, `engine.start`, `connectors.sync` and `material.import` with `autoAdvance` now need a live key on a paid plan, and `projects.create` was removed from the API entirely. The refusal is a `403` with `code: "live_key_required:<reason>"` and a sentence explaining it. If your integration used a test key for contact lookup, this is a breaking change and it is deliberate. **Added to `GET /mcp/capabilities`:** `sandboxAllowed`, `liveOnlyReason` and `requiresEntitlement` per capability, so the limits are readable rather than discoverable one error at a time. **Documented:** the error envelope and every `code`, per key rate and size limits, what actually consumes credits, a single canonical transport answer, and [What MCP does not do](#what-mcp-does-not-do). --- ## Source files | File | Role | |------|------| | `api/mcp.ts` | Hosted remote MCP server (mcp.selda.ai, Vercel Edge, Streamable HTTP) | | `mcp-server/index.ts` | Node stdio MCP server, tool definitions, HTTP client | | `mcp-server/handlers.ts` | Extra tool handlers (analysis, discussions, channels, drafts) | | `mcp-server/setup.ts` | Interactive client configuration | | `convex/mcpApi.ts` | HTTP handlers for `/mcp/query`, `/mcp/mutate`, `/mcp/run`; auth + scope + org injection | | `convex/mcpQueries.ts` | Org-scoped internal queries/mutations/actions the HTTP layer calls | | `convex/apiKeys.ts` | API key creation, validation, revocation (SHA-256 hashed storage) | | `convex/http.ts` | Routes the MCP endpoints | --- <!-- https://docs.selda.ai/security --> # Security & privacy Selda is built so your data stays yours and isolated to your workspace. - **Workspace isolation.** Every workspace is its own organization; data (projects, leads, campaigns, inbox, credits) is scoped to it. Server-side access checks gate every read and write. - **Auth.** Sign-in is handled by Clerk (JWT). API/MCP access uses revocable API keys (`sk_live_…`), stored hashed, scoped to one organization. - **Billing integrity.** Subscription and credit changes are server-only and verified against Stripe, and cannot be altered from the client. - **Webhooks** from providers (Stripe, Clerk, Mailgun) are signature-verified. - **You approve sends.** Selda drafts outreach; nothing is sent without your approval. For how outreach data is sourced and how opt-outs are honored, see [How Selda contacts people](/how-selda-contacts-people). For questions about data processing or compliance, contact [support](/support). --- <!-- https://docs.selda.ai/how-selda-contacts-people --> # How Selda contacts people Plain answers about where contact data comes from, on what basis Selda reaches out, and what happens when someone says no. Everything on this page describes how the product actually behaves. ## Where contact data comes from Selda researches companies and decision-makers from **publicly available sources**: - **The company's own website**, most contact details come straight from public pages (contact pages, team pages, imprints). - **Public business registries and directories**, company records and public listings (for example business registries and map/directory listings). - **Professional contact databases**, publicly listed business contacts (for example Hunter). - **Data you bring**, contacts you import or add yourself. Each lead in your CRM shows where its contact info came from. For leads researched before this tracking existed, Selda shows a neutral "found during research" note instead of guessing. Selda researches **business contacts at companies** (B2B). It does not buy consumer lists or scrape private data. ## The basis for reaching out Selda's outreach model is 1:1, researched, business-to-business contact: a small number of high-fit companies, each message written from real research about that company, sent from your own connected inbox. This is the model commonly operated under **legitimate interest** for B2B outreach in the EU. Two honest caveats: - Whether legitimate interest applies to *your* outreach depends on your business, your audience and your jurisdiction. **This page is not legal advice, consult your own counsel.** - You are the sender. Selda drafts and researches; nothing is sent without a human approving it first. ## What happens when someone opts out When a recipient replies asking not to be contacted, Selda handles it automatically: 1. The reply is classified as an opt-out. 2. The contact is added to a **permanent suppression registry**, they will never be contacted by that workspace again, and their contact details are never re-collected by Selda's research. 3. Their personal data (name, email, phone, social profiles, conversation history) is **erased from Selda's research database**. 4. Any active follow-up sequences to that person are **stopped immediately**. The thread in your Sales Inbox shows an "Opted out" marker so you can see it was honored. You can also do this manually: every lead in the CRM has a **"Do not contact"** action that triggers the same suppression, erasure and sequence stop. ## Data deletion - **Opt-out or "Do not contact"** erases the person's personal fields from Selda's research database and permanently suppresses the contact (the suppression entry itself is kept, it is what guarantees they are never contacted again). - **Deleting your account** removes your account data (Settings → Account). - For **data subject requests** (erasure, access, objection) beyond the built-in flows, contact [support](/support), suppression and erasure are applied to the global research database, not just one workspace. ## Questions For anything not covered here, contact [support](/support). --- <!-- https://docs.selda.ai/faq --> # FAQ **Is Selda just a cold-email tool?** No. Selda is a go-to-market engine. It understands your product first, finds the right audience, writes relevant messages, and runs the channels that fit. Email and LinkedIn today. **Does Selda send automatically?** No. Selda drafts. You approve before anything is sent. **Which channels are supported?** Email and LinkedIn, both from **your own connected account**. Selda does not send from its own infrastructure, so replies land in your inbox and your domain keeps its own reputation. Forums (Reddit, Hacker News, Discord, X) are available as an add-on, where Selda writes the reply and you post it yourself. **What are credits?** Credits are Selda's work, drawn from one pool per organisation: researching a company, finding the decision-maker and writing a message each cost a few. Receiving replies is always free. A free workspace gets a one-time welcome grant of 30 credits, about one full run, end to end, and no recurring allowance; paid plans include a monthly amount, and unused plan credits carry one period forward. You can buy a top-up pack at any time. **Can I use Selda from Claude or ChatGPT?** Yes, via the [MCP server](/ways-to-use/mcp-server). **Can my team collaborate?** Yes, on Business, which is seat-based. Everyone in a workspace shares its projects, inbox and credit pool. ### Can I add capabilities I need without changing plan? Yes, and that is how the pricing works: the whole engine is in every paying plan, and the channels are add-ons, each at the same monthly price. **LinkedIn**, **WhatsApp**, **Selda Forums** and **inbound intake** are added to the plan you are on rather than reached by moving to a bigger one. Forums, for example, finds the conversations where your people are already asking, writes the reply, and watches the thread for answers after you post it. ### Does Selda post in forums for me? No, and it never will. Posting in a community means posting as a person, and that person is you. Selda finds the thread and writes the draft; you post it under your own account and tell Selda you did, and it then reads the public thread for replies. It never signs in as you and never reads your private messages. --- <!-- https://docs.selda.ai/glossary --> # Glossary - **Workspace**: your go-to-market home (one organization). Holds your product context, campaigns, leads, and inbox. - **Project**: the product/company a workspace is doing GTM for. - **ICP**: ideal customer profile; the audience Selda targets. - **Lead**: a company + decision-maker Selda found and researched. - **Campaign / run**: one execution of the engine that discovers leads and drafts outreach. - **Pipeline**: the engine flow: URL → audience → discovery → research → message. - **Sales Inbox**: where inbound replies land and get handled. - **Credits**: the org-level usage currency for engine work. - **Channel**: a way to reach prospects (email, LinkedIn). - **MCP**: Model Context Protocol; lets external AI tools use Selda. --- <!-- https://docs.selda.ai/support --> # Support ## Chat with us The fastest route is the **Chat with us** button in the corner of this page, and the same chat inside the Selda app. From the app we can see which workspace you are in, so you do not have to explain the setup before we can help. The chat loads only when you press that button, so nothing is running in the background while you read. Declining optional cookies does not take this away. ## Email **[support@selda.ai](mailto:support@selda.ai)** if you would rather write, if you are not signed in yet, or if the chat is blocked on your machine. It reaches the same people. When you report an issue, include your workspace, what you expected, what actually happened, and a link or a screenshot. That one paragraph usually turns two days of back and forth into one reply. ## Community **[Discord](https://discord.com/invite/4fWkGnbS3P)** is where builders using Selda talk to each other: what worked, what did not, and what they are shipping. It is a community, not a support queue. If something is broken or blocking you, use the chat or email above so it reaches us and gets an answer.