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.
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>" } }'{
"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.
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.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:
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.
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>" } }'{
"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.