ReferenceCalling it over REST

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 is OpenAPI 3.1, generated from the same registry. Import it into Postman, or point a client generator at it.

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.

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

MethodPathfnWhat it does
GET/v1/brainbrain.listThe 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/brainbrain.addAdd 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/removebrain.removeTake one Brain item back out. The human owns what Selda knows.
POST/v1/brain/updatebrain.updateRewrite the title and body of one Brain item.

campaigns

MethodPathfnWhat it does
GET/v1/campaignscampaigns.listCampaigns in a workspace.
POST/v1/campaignscampaigns.createCreate a campaign (legacy table, not the one the app’s campaign-flow UI reads).
GET/v1/campaigns/{campaignId}campaigns.getOne campaign: status, channels, settings, leads.
PATCH/v1/campaigns/{campaignId}campaigns.updateChange a campaign.
POST/v1/campaigns/{campaignId}/add-leadscampaigns.addLeadsPut specific leads into a campaign.
POST/v1/campaigns/{campaignId}/lock-message-structurecampaigns.lockMessageStructureLock 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-structurecampaigns.messageStructureRead 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-structurecampaigns.setMessageStructureState 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}/statscampaigns.statsCampaign counters: sent, delivered, opened, clicked, replied, bounced.
POST/v1/campaigns/add-leads-by-tagcampaigns.addLeadsByTagPut every lead carrying a tag into a campaign (legacy table).
POST/v1/campaigns/add-rulecampaigns.addRuleAdd a campaign rule.

company

MethodPathfnWhat it does
POST/v1/company/lookupcompany.lookupResolve a company and return the right people to reach. Starts nothing. (live key only)

connectors

MethodPathfnWhat it does
GET/v1/connectorsconnectors.listData connectors registered for this workspace.
POST/v1/connectorsconnectors.createRegister a data connector.
DELETE/v1/connectors/{connectorId}connectors.deleteRemove a data connector.
POST/v1/connectors/{connectorId}/syncconnectors.syncPull from a connected data source. (live key only)

credits

MethodPathfnWhat it does
GET/v1/credits/infocredits.infoCredit balance, daily free credits, usage, plan.

drafts

MethodPathfnWhat it does
POST/v1/drafts/removedrafts.removeTake 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/updatedrafts.updateRewrite the draft on one run lead. Refuses a message that already went out; never sends.

engine

MethodPathfnWhat it does
POST/v1/engine/startengine.startThe 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

MethodPathfnWhat it does
POST/v1/events/ingestevents.ingestReport 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

MethodPathfnWhat it does
GET/v1/flowsflows.listThe 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/flowsflows.createCreate 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.deleteDelete a flow and its run log.
PATCH/v1/flows/{flowId}flows.updateRewrite a flow’s name, trigger or steps.
GET/v1/flows/{flowId}/runsflows.runsWhat a flow actually did, run by run, step by step, including the steps that did nothing and why.
POST/v1/flows/{flowId}/set-enabledflows.setEnabledSwitch a flow on or off.
POST/v1/flows/save-skillflows.saveSkillWrite or rewrite an instruction file a flow step reads: how this business decides what an enquiry is.

inbox

MethodPathfnWhat it does
POST/v1/inbox/add-messageinbox.addMessagePut 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

MethodPathfnWhat it does
GET/v1/knowledgeknowledge.getWhat Selda knows about your business: the prose that grounds every message.
POST/v1/knowledge/appendknowledge.appendAdd to what Selda knows about your business.
POST/v1/knowledge/setknowledge.setReplace what Selda knows about your business.

leads

MethodPathfnWhat it does
GET/v1/leadsleads.listLeads in a workspace.
POST/v1/leadsleads.addAdd 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.deleteRemove one lead. Deleting is the caller’s act, Selda never removes a lead on its own.
GET/v1/leads/{leadId}leads.getOne lead in full: research, fit, outreach angle, notes.
PATCH/v1/leads/{leadId}leads.updateEdit a lead’s fields, including its status. Org-scoped, so an API key can reach it.
POST/v1/leads/{leadId}/add-aliasleads.addAliasClaim 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-tagleads.addTagTag a lead.
POST/v1/leads/{leadId}/enrichleads.enrichEnrich one lead from a natural-language instruction. (live key only)
POST/v1/leads/{leadId}/skipleads.skipDELETES 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}/statusleads.updateStatusSet 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-batchleads.addBatchAdd many companies/contacts in one call.
POST/v1/leads/delete-batchleads.deleteBatchRemove many leads.
POST/v1/leads/enrich-batchleads.enrichBatchEnrich many leads from a natural-language instruction. (live key only)
POST/v1/leads/mergeleads.mergeMerge duplicate leads.

material

MethodPathfnWhat it does
POST/v1/material/importmaterial.importYour prospect folder → a campaign + company list, then it stops. Uploading material is not permission to send.

messages

MethodPathfnWhat it does
POST/v1/messages/{messageId}/approvemessages.approveApprove a drafted message. Approval only. It does not send.
GET/v1/messages/by-leadmessages.byLeadThe whole thread with one lead, sent and received.
GET/v1/messages/by-projectmessages.byProjectMessages in a workspace.
POST/v1/messages/generatemessages.generateDraft a message for a lead.

projects

MethodPathfnWhat it does
GET/v1/projectsprojects.listYour workspaces. Start here. Every other fn needs a projectId.
GET/v1/projects/{projectId}projects.getOne workspace in full: business context, market analysis, ICP, settings.
PATCH/v1/projects/{projectId}/contextprojects.updateContextRewrite a workspace’s business context.

replies

MethodPathfnWhat it does
POST/v1/replies/classifyreplies.classifyClassify inbound replies.
POST/v1/replies/draftreplies.draftWrite 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/previewreplies.previewAsk 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

MethodPathfnWhat it does
GET/v1/runsruns.listEvery 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}/archiveruns.archiveClose 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-companiesruns.confirmCompaniesConfirm 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}/leadsruns.leadsThe companies a run found, each with the message Selda drafted for it. Nothing is sent.
POST/v1/runs/{runId}/renameruns.renameGive a campaign run a name a person would recognise. An empty name restores the derived title.
GET/v1/runs/{runId}/statusruns.statusStatus of one campaign run: phase, companies found, contacts resolved, drafts written, errors.
POST/v1/runs/start-from-leadsruns.startFromLeadsStart 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

MethodPathfnWhat it does
GET/v1/webhookswebhooks.listOutbound webhook endpoints registered for this workspace.
POST/v1/webhookswebhooks.createRegister an endpoint for events like reply.received.
DELETE/v1/webhooks/{webhookId}webhooks.deleteRemove a webhook endpoint.

material upload

MethodPathWhat it does
POST/v1/material/uploadRaw 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.

fnCall it here insteadWhy
leads.enrichPOST /mcp/runanother 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.updatePOST /mcp/mutateanother 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.draftPOST /mcp/runPOST /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.