CRM API
Leads, deals, pipelines, companies, tasks and notes, plus the CRM configuration. Requires a plan with the CRM
(403 FEATURE_NOT_AVAILABLE otherwise). Product behaviour: CRM guide. Full schemas: the
OpenAPI reference.
Scopes
| Scope | Allows |
|---|---|
leads:read / leads:write | Read / capture, update, assign, convert leads; notes and tasks |
deals:read / deals:write | Read / manage deals and the board; read pipelines |
companies:read / companies:write | Read / manage and merge companies |
crm:admin | Settings, pipelines, custom fields, scoring and assignment rules, forms and links |
Converting a lead needs leads:write and deals:write.
Leads
Capture a lead
curl -X POST https://api.whatstig.com/v1/leads \
-H "Authorization: Bearer $WHATSTIG_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: signup-8842" \
-d '{
"phone": "+923001234567",
"firstName": "Sara", "lastName": "Ahmed", "email": "sara@acme.io", "title": "CTO",
"company": { "name": "Acme", "domain": "acme.io" },
"source": "api",
"sourceDetail": { "label": "Pricing page", "utm": { "source": "google", "campaign": "q4" } },
"customFields": { "budget": 2500 },
"consent": { "status": "opted_in", "source": "form", "proof": { "text": "I agree to WhatsApp updates", "url": "https://acme.io/pricing" } },
"note": "Asked for a demo next week"
}'
200 with { "lead": Lead, "created": true|false, "consentIgnored": false }. A person who already has an open lead
gets it updated (created: false): new details fill in, but values a person set in the dashboard are never
overwritten by the API. Pass contactId instead of phone for a known contact. Idempotency-Key makes retries safe.
Read and change
| Method | Path | |
|---|---|---|
GET | /v1/leads | Filters: status (open = new/working/qualified), qualification, source, ownerUserId (none), companyId, contactId, minScore, createdAfter, createdBefore, slaBreached, q; sort: created_desc, created_asc, score_desc, last_activity_desc |
GET | /v1/leads/{id} | |
PATCH | /v1/leads/{id} | Names, email, title, companyId, customFields (merged; null clears), qualification override (null = back to scoring), version (409 on a stale view) |
POST | /v1/leads/{id}/status | { "status": "working" | "qualified" | "unqualified" | "archived" | "new", "reason" } (allowed moves only: INVALID_TRANSITION) |
POST | /v1/leads/{id}/assign | { "ownerUserId": "…" | null, "teamId" } |
POST | /v1/leads/{id}/convert | { "deal": { "title", "pipelineId", "stageId", "valueMinor", "currency", "expectedCloseDate", "ownerUserId" }, "companyId" | "company" } → { lead, deal }. Idempotent with a key; a converted lead cannot be converted again |
POST | /v1/leads/{id}/rescore | Recalculate now |
POST | /v1/leads/{id}/ai-extraction | Queue an AI pass over the lead's conversation (202) |
GET | /v1/leads/{id}/timeline | Activities and notes, newest first |
POST | /v1/leads/bulk | Up to 500: { "leadIds": [...], "action": { "type": "assign", "ownerUserId" } | { "type": "status", "status", "reason" } | { "type": "delete" } } |
GET | /v1/leads/export | CSV of the leads matching the list filters (max 50,000) |
DELETE | /v1/leads/{id} | The contact and conversations are kept |
Every lead carries score, scoreBreakdown (matched rules), qualification, source and sourceDetail (UTM,
form, link, instance), fieldSources (who set each field: human, api, form, import, ai with confidence),
ai insights (summary, intent, urgency, sentiment, missing details) and the speed-to-lead timestamps
(firstResponseDueAt, firstRespondedAt, slaBreachedAt).
Deals and pipelines
| Method | Path | |
|---|---|---|
GET / POST | /v1/deals | List (filters: pipelineId, stageId, status, ownerUserId, companyId, contactId, leadId, q) / create |
GET | /v1/deals/board?pipelineId=&perStage= | Deals per stage in board order with counts and value totals (plain and weighted) per currency |
GET / PATCH / DELETE | /v1/deals/{id} | |
POST | /v1/deals/{id}/move | { "stageId", "afterDealId", "lostReason", "version" } — Won/Lost stages close the deal |
POST | /v1/deals/{id}/close | { "status": "won" | "lost", "lostReason" } |
GET | /v1/deals/{id}/timeline | |
GET / POST | /v1/pipelines | Every project has a default "Sales" pipeline |
PATCH / DELETE | /v1/pipelines/{id} | Rename, archive, make default, or replace the ordered stages (moveDealsTo for removed stages with deals) |
Amounts are integers in the currency's minor unit (valueMinor: 250000 = USD 2,500.00).
Companies, notes, tasks
/v1/companies (CRUD, POST /v1/companies/{id}/merge { "sourceCompanyId" }, timeline);
POST /v1/crm/notes { subjectType, subjectId, body }; /v1/crm/tasks (list with assigneeUserId, status,
due=overdue|today|upcoming|none, subjectType, subjectId; create, PATCH { "status": "done" }, delete).
Configuration (crm:admin)
GET/PATCH /v1/crm/settings (capture mode and keywords, AI, qualification thresholds, SLA, assignment, visibility,
currency), /v1/crm/fields, /v1/crm/scoring-rules (+ POST …/preview), /v1/crm/assignment-rules,
/v1/lead-forms, /v1/lead-links, GET /v1/crm/reports.
Rules use a JSON condition (LeadRule): a group { "type": "group", "op": "and" | "or", "rules": [...] } of
conditions such as { "type": "custom_field", "key": "budget", "op": "gte", "value": 1000 },
{ "type": "source", "op": "is", "values": ["form"] }, { "type": "engagement", "metric": "inbound_messages", "op": "gte", "value": 3 },
{ "type": "ai", "signal": "intent", "op": "is", "values": ["pricing", "demo"] }, { "type": "phone", "op": "starts_with", "value": "971" }.
At most 4 levels, 50 conditions (RULE_TOO_COMPLEX).
Webhooks
| Event | When |
|---|---|
lead.created | A lead was captured (any source) |
lead.updated | Fields changed (data.changes names them) |
lead.status_changed | New status (data.previousStatus) |
lead.qualified | The lead became sales-qualified |
lead.assigned | Owner set or changed |
lead.converted | Converted (data.dealId) |
lead.sla_breached | First-response target missed |
deal.created, deal.updated, deal.stage_changed, deal.won, deal.lost | Deal lifecycle (data.stageId, data.previousStageId, data.valueMinor, data.currency) |
company.created, company.updated | |
task.created, task.due, task.completed |
Payloads carry IDs, statuses and changed field names — never message content. Fetch the record for details.
Errors
LEAD_LIMIT_REACHED, PIPELINE_LIMIT_REACHED, FORM_LIMIT_REACHED (403, plan limits), INVALID_FIELD_VALUE,
RULE_TOO_COMPLEX (400), INVALID_TRANSITION, STAGE_NOT_IN_PIPELINE, CONFLICT (409; stale version, duplicate
externalId/domain, second open lead), plus the public form errors FORM_NOT_ACCEPTING (409), ORIGIN_NOT_ALLOWED
(403) and CAPTCHA_FAILED (400).