Connect your app
One endpoint pattern, one auth header, no SDK required. Works from any backend, any language. Push data in, read your pipeline out, receive events back when things happen.
Quickstart (3 min)
1. Get your key. Settings → Integrations → Connect your app → Create a key.
SELDA_API_KEY=sk_live_...
SELDA_PROJECT_ID=...2. Push a lead:
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": "Acme Oy",
"email": "owner@acme.fi",
"companyDomain": "acme.fi",
"source": "my-app"
}
}'{ "value": { "leadId": "...", "duplicate": false } }Selda dedupes by email, re-running is safe. That’s the whole integration.
Using an AI tool? Hit “Copy setup prompt” in the app and paste it into Claude Code / Cursor / Lovable with your product, it wires the integration for you.
Four integration patterns
1. Push data to Selda
Send leads from any backend event or cron job:
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: "Acme Oy",
email: "owner@acme.fi",
companyDomain: "acme.fi",
notes: "views:1200 · no_video:true",
source: "my-app",
},
}),
});Or hand Selda a campaign brief and let the engine find decision-makers:
# 1. Create campaign
POST /mcp/mutate { "fn": "campaigns.create", "args": { "projectId": "...", "name": "Q3 push", "leadCount": 30 } }
# 2. Run engine (finds leads, writes messages, waits for approval, nothing sends automatically)
POST /mcp/run { "fn": "engine.start", "args": { "projectId": "..." } }Push a lead with your own analysis
If you already researched the company (for example, Claude wrote a per-company
analysis), 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. This
is the Claude × Selda loop, you supply the depth, Selda finds the contact,
writes the message, and holds it for your approval.
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: "Acme Oy",
companyDomain: "acme.fi",
// Your write-up (markdown or plain text). The message stays faithful to it. // Selda never invents specifics beyond what you provide here.
analysis: "Acme 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",
},
}),
});Via the hosted MCP server the same lands as the analysis parameter on the
selda_add_lead tool. Then run the engine (or add the lead to a campaign): it
finds the right contact, grounds the hook and message on your analysis, and waits
for your approval before anything sends.
Faithful by design. The
analysistext 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.
2. Read your pipeline
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":"..."}}'
# → { "value": { "sent": 42, "replies": 7, "meetings": 2 } }Full query list: Read reference below.
3. Receive events from Selda (outbound webhooks)
Register a URL and Selda POSTs a signed payload whenever something happens.
Register (or: Settings → Integrations → Outbound webhooks → Add):
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://your-app.com/selda/events",
"events": ["reply.received", "lead.status_changed"],
"description": "My app"
}
}'
# → { "value": { "webhookId": "...", "secret": "whsec_..." } }Available 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 | Lead status changes to responded / qualified, or the CRM stage moves (contacted → replied → meeting → customer / lost) |
campaign.completed | A campaign run finishes sending |
credits.low | Credit 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, not a fixed 20 |
meeting.booked | A prospect books a meeting through Selda |
test.ping | Sent by the “Send test” button / sendTest, verify your endpoint |
Subscribe with "events": ["*"] to receive everything.
Payload, every delivery POSTs { event, timestamp, data } with two headers:
X-Selda-Event (the event name) and X-Selda-Signature (see below). Example for
reply.received:
{
"event": "reply.received",
"timestamp": 1720000000000,
"data": {
"inboundMessageId": "...",
"leadId": "...",
"email": "owner@acme.fi",
"company": "Acme Oy",
"campaignId": "...",
"from": "owner@acme.fi",
"subject": "Re: Quick question",
"classification": "positive"
}
}Verify the signature (Node.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
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 guarantees: 10-second timeout per request. On a non-2xx response or
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. After 10 consecutive
deliveries where all attempts fail, the endpoint is auto-disabled (active: false,
disabledAt set), the state is visible via webhooks.list and in Settings →
Outbound webhooks, where you can re-enable the endpoint once your receiver is fixed.
4. Pull from your app (data connectors)
Selda polls your HTTP JSON endpoint and syncs records as leads automatically.
Register (or: Settings → Integrations → Apps → “Connect your API”):
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.myapp.com/customers",
"authHeader": "Bearer sk_...",
"fieldMap": {
"company": "companyName",
"email": "ownerEmail",
"companyDomain": "website",
"notes": "city"
}
}
}'Your endpoint must return a JSON array. Selda maps fields via fieldMap, dedupes by email, polls on a schedule.
Update workspace knowledge and playbooks
Push context into Selda so the engine writes better, more targeted messages.
Set workspace knowledge (what your product is, what you sell, ICP):
# Append to existing knowledge
POST /mcp/mutate
{
"fn": "knowledge.append",
"args": {
"projectId": "...",
"text": "We sell media packages: video production 990€, distribution to 14k daily readers. Target: terrace restaurants without a video yet."
}
}fn | Does |
|---|---|
knowledge.get | Read current knowledge notes |
knowledge.set | Replace the whole knowledge field |
knowledge.append | Append text (keeps existing content) |
Update a campaign playbook mid-run:
POST /mcp/mutate
{
"fn": "campaign.updatePlaybook",
"args": {
"runId": "...",
"appendText": "Prioritise restaurants with views > 1000 but no video yet."
}
}runId comes from campaigns.list → each campaign’s runs array.
API reference
Authentication
Authorization: Bearer sk_live_...Your org is resolved from the key, never send orgId.
| Endpoint | Scope required |
|---|---|
POST /mcp/query | read |
POST /mcp/mutate | write |
POST /mcp/run | pipeline |
Missing scope → 403 { "error": "Missing 'write' scope" }.
Write. POST /mcp/mutate
Leads
fn | Key args | Does |
|---|---|---|
leads.add | projectId, company, email, companyDomain, firstName, lastName, jobTitle, linkedinUrl, phone, notes, source, tags[] | Add a lead. Dedupes by email. Triggers auto-routing rules. |
leads.addBatch | projectId, leads[] (max 500) | Batch insert. Returns [{ leadId, duplicate }]. Triggers auto-routing per lead. |
leads.update | leadId + any lead fields | Patch lead fields. Only provided fields are updated. |
leads.updateStatus | leadId, status | Set status: new / contacted / responded / qualified / unqualified. |
leads.addTag | leadId, tags[] | Add tags (merges, no duplicates). |
leads.archive | leadId | Soft-delete a lead. |
Campaigns
fn | Key args | Does |
|---|---|---|
campaigns.create | projectId, name, outreachAngle, channels[], targetRole, targetIndustry, targetCompanySize, locations[], leadCount, startAt? | Create a campaign. startAt (unix ms) schedules the engine start. |
campaigns.update | campaignId, name?, description? | Rename or update description. |
campaigns.delete | campaignId | Soft-delete (blocks if active run). |
campaigns.addLeads | campaignId, leadIds[] | Attach leads to a campaign. |
campaigns.pause | runId | Pause a running engine run. |
campaigns.resume | runId | Resume a paused run. |
campaigns.addRule | projectId, campaignId, condition: { tag } | Auto-route: leads tagged with this go to this campaign automatically. |
campaigns.deleteRule | ruleId | Remove an auto-routing rule. |
campaigns.sendWindow | campaignId, sendWindow: { days, startHour, endHour }, maxPerDay? | Set sending schedule. days: ["monday","tuesday",...]. |
Messages
fn | Key args | Does |
|---|---|---|
messages.approve | messageId, sendAt? | Approve for sending. Pass sendAt (unix ms) to schedule. |
messages.approveAll | projectId, campaignId?, sendAt? | Approve all pending drafts (optionally for one campaign). |
messages.reject | messageId, reason? | Reject a draft. |
messages.edit | messageId, body?, subject? | Edit draft body/subject before sending. |
Replies
fn | Key args | Does |
|---|---|---|
replies.markHandled | replyId | Mark an inbound reply as handled. |
Knowledge & playbooks
fn | Key args | Does |
|---|---|---|
knowledge.set | projectId, text | Replace workspace knowledge. |
knowledge.append | projectId, text | Append to workspace knowledge. |
campaign.setKnowledge | campaignId, text | Replace campaign knowledge + update latest run’s playbook. |
campaign.appendKnowledge | campaignId, text | Append campaign knowledge. |
campaign.updatePlaybook | runId, playbook?, appendText? | Replace or append to the campaign brief. |
Projects, connectors, webhooks
fn | Key args | Does |
|---|---|---|
projects.create | name, websiteUrl, description | Create a workspace. |
projects.updateContext | projectId + context fields | Update ICP, voice, competitors. |
connectors.create | projectId, name, url, authHeader?, fieldMap | Register a data connector (Selda polls your API). |
connectors.delete | connectorId | Remove a connector. |
webhooks.create | url, events[], description? | Register an outbound webhook. Returns secret once. |
webhooks.delete | webhookId | Remove a webhook. |
Read. POST /mcp/query
fn | Args | Returns |
|---|---|---|
projects.list | . | Your projects |
projects.get | projectId | One project |
leads.list | projectId, limit?, cursor?, tags[]?, status? | Leads (paginated, filterable by tag + status) |
leads.get | leadId | One lead |
leads.stats | projectId | { total, byStatus, byTag }, counts grouped by status and tag |
campaigns.list | projectId | Campaigns |
campaigns.get | campaignId | One campaign |
campaigns.stats | campaignId | { sent, replies, meetings } |
campaigns.statsDetailed | campaignId | { total, byStatus, byDay: [{ date, sent, replied }] }, daily breakdown |
campaigns.sendWindow | campaignId | Send window config + maxPerDay |
campaigns.rules.list | projectId | Active auto-routing rules |
messages.list | projectId, status?, campaignId?, limit? | Messages filtered by status and/or campaign |
messages.byProject | projectId | All messages and drafts |
messages.byLead | leadId | Thread for one lead |
replies.list | projectId, status?, campaignId?, limit?, cursor? | Inbound replies (paginated) |
credits.info | . | Remaining credits |
connectors.list | projectId | Active data connectors |
webhooks.list | . | Registered outbound webhooks |
knowledge.get | projectId | Current knowledge notes |
Run. POST /mcp/run
fn | Does |
|---|---|
engine.start | Full discovery → research → hook → message pipeline. Pass startAt (unix ms) to delay start. It stops at the company list and waits for a person to confirm it in the app — poll runs.status and read awaitingHuman. |
company.lookup | Enrich a single company |
messages.generate | Draft a message |
messages.send | Send a message |
replies.classify | Classify an inbound reply |
replies.draft | Draft a reply |
connectors.sync | Force-sync a data connector now |
Errors
| Status | Meaning |
|---|---|
401 | Missing or revoked key |
403 | Key lacks required scope |
400 | Unknown fn |
500 | Function threw (e.g. project not found) |
End-to-end example: terassikesä
New restaurant signs up → Selda reaches out → reply fires back to your app.
1. New restaurant on terassikesa.fi
→ POST /mcp/mutate fn=leads.add
{ company: "Huvila Ranta", email: "jani@huvila.fi", notes: "views:1200 · no_video:true" }
2. Selda engine runs, finds contact, writes personalised message, waits for approval
3. You approve in the Selda app → Selda sends
4. Jani replies → Selda fires webhook:
POST https://adventurous-bear-355.eu-west-1.convex.site/webhooks/selda
{
"event": "reply.received",
"data": {
"company": "Huvila Ranta",
"email": "jani@huvila.fi",
"classification": "positive",
"campaignId": "..."
}
}
5. Your backend marks the restaurant as a hot lead| terassikesä field | Selda field |
|---|---|
restaurantName | company |
website | companyDomain |
ownerEmail | email |
ownerFirstName | firstName |
ownerLastName | lastName |
city | notes |
Source of truth: convex/mcpApi.ts. See Security for key and webhook secret handling.