REST APIExamples

Examples

Every example on this page is a complete, runnable call against https://api.selda.ai/v1/…. Set two variables and paste. Each one also has an RPC form, POST /mcp/query|mutate|run with a fn in the body, shown on that function’s own page in the reference.

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.


Add one lead

The most common call there is. Selda dedupes by email, so running it twice is safe.

curl -X POST https://api.selda.ai/v1/leads \
  -H "Authorization: Bearer $SELDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "'"$SELDA_PROJECT_ID"'",
    "company": "Example Oy",
    "email": "owner@example.fi",
    "companyDomain": "example.fi",
    "source": "my-app"
  }'

The response always has the same four fields, so your code never has to branch on which path it took:

{ "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

curl -X POST https://api.selda.ai/v1/leads/add-batch \
  -H "Authorization: Bearer $SELDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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.

curl -X POST https://api.selda.ai/v1/events/ingest \
  -H "Authorization: Bearer $SELDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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.


Read your pipeline

curl "https://api.selda.ai/v1/campaigns/<campaignId>/stats" \
  -H "Authorization: Bearer $SELDA_API_KEY"
{ "value": { "sent": 42, "replies": 7, "meetings": 2 }, "request_id": "req_..." }

Every read is a GET, and its arguments go in the query string:

What you wantPath
Leads in a workspace, filtered by tag or statusGET /v1/leads?projectId=...
One lead in full, with its research and angleGET /v1/leads/{leadId}
Every message in a workspaceGET /v1/messages/by-project?projectId=...
The whole thread with one person, both directionsGET /v1/messages/by-lead?leadId=...
Campaign runs, newest firstGET /v1/runs?projectId=...
A run’s companies with the draft written for eachGET /v1/runs/{runId}/leads
Credit balance, usage and planGET /v1/credits/info

All of them, with their arguments.


Run a campaign from a brief

# 1. Create the campaign
curl -X POST https://api.selda.ai/v1/campaigns \
  -H "Authorization: Bearer $SELDA_API_KEY" -H "Content-Type: application/json" \
  -d '{"projectId":"'"$SELDA_PROJECT_ID"'","name":"Q3 push","leadCount":30}'
 
# 2. Start the engine. Needs a live key: it spends.
curl -X POST https://api.selda.ai/v1/engine/start \
  -H "Authorization: Bearer $SELDA_API_KEY" -H "Content-Type: application/json" \
  -d '{"projectId":"'"$SELDA_PROJECT_ID"'"}'
 
# 3. Poll, and read awaitingHuman before you say it finished
curl "https://api.selda.ai/v1/runs/<runId>/status" \
  -H "Authorization: Bearer $SELDA_API_KEY"

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 /v1/leads
   { 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 fieldSelda field
restaurantNamecompany
websitecompanyDomain
ownerEmailemail
ownerFirstNamefirstName
ownerLastNamelastName
citynotes

Step 4 needs a webhook: how to register and verify one.


Teach Selda about your business

The engine writes better when it knows what you sell. This is the same Brain the app edits.

curl -X POST https://api.selda.ai/v1/knowledge/append \
  -H "Authorization: Bearer $SELDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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."
  }'

GET /v1/knowledge reads it, POST /v1/knowledge/set replaces the whole thing, and the call above adds to it. The engine re-reads it, so a campaign already running picks the change up on its next lead.

Steering the shape of the messages themselves is POST /v1/campaigns/{campaignId}/set-message-structure: which blocks ship word for word, which ones Selda writes from an instruction you give it, and what must never appear. POST /v1/campaigns/{campaignId}/lock-message-structure freezes it. Neither sends anything.


When something goes wrong

StatusWhat it meansWhat to do
401Missing or revoked keyCheck the header, make a new key
403The key lacks the scope, the plan, or is a sandbox key on something that spendsThe code says which of the three
400Arguments the validator refused, or an unknown fn in the RPC formCheck them against the reference
404No such pathCheck it against every REST path
429Too many callsWait and retry. The limit is per key
500The function threwThe 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.