leads.add
Add one company/contact. Pass
analysiswith research you already did and the message is written from it instead of a fresh crawl.
| REST | POST /v1/leads |
| RPC form | POST /mcp/mutate with "fn": "leads.add" |
| Scope | write |
| Sandbox key | allowed |
| Extra entitlement | none |
| MCP tools | selda_add_lead |
| Internal 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 | |
mediaImageUrl | string | optional | A PICTURE FOR THIS ONE PERSON’S MESSAGE (15.9.2026). mediaImageUrl is a public https address, a gif rendered for this company, a screenshot of their own site. mediaLinkUrl makes it clickable, which is how a gif opens a video. It goes into THIS lead’s draft when the draft is composed, so it is in the text the human approves, and it beats any picture set on the campaign as a whole. A picture never costs a lead: an address that cannot be used is dropped, the lead is created anyway, and the answer says in words what was not kept and why (mediaRejected). |
mediaLinkUrl | string | optional | |
mediaAlt | string | optional | |
mediaWidth | number | optional | |
source | string | optional | |
tags | string[] | optional | |
city | string | optional | |
country | string | optional | |
location | string | optional | |
customFields | any | optional | |
fields | Record<string, any> | optional | Your own data for this company’s personal page, e.g. { demo: "https://...", offer: "...", price: 49 }. |
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.
curl -X POST https://api.selda.ai/v1/leads \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "projectId": "<projectId>" }'{
"projectId": "<projectId>"
}The same call in the RPC form
One dispatcher answers both, so this is the same request at a different URL. Use it when you
are already posting to /mcp/*, or for a function that has no path of its own.
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>" } }'{
"fn": "leads.add",
"args": {
"projectId": "<projectId>"
}
}Example response
Every endpoint answers in the same envelope, whichever URL you called.
{
"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.