Skip to content

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

ScopeAllows
leads:read / leads:writeRead / capture, update, assign, convert leads; notes and tasks
deals:read / deals:writeRead / manage deals and the board; read pipelines
companies:read / companies:writeRead / manage and merge companies
crm:adminSettings, 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

MethodPath
GET/v1/leadsFilters: 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}/rescoreRecalculate now
POST/v1/leads/{id}/ai-extractionQueue an AI pass over the lead's conversation (202)
GET/v1/leads/{id}/timelineActivities and notes, newest first
POST/v1/leads/bulkUp to 500: { "leadIds": [...], "action": { "type": "assign", "ownerUserId" } | { "type": "status", "status", "reason" } | { "type": "delete" } }
GET/v1/leads/exportCSV 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

MethodPath
GET / POST/v1/dealsList (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/pipelinesEvery 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

EventWhen
lead.createdA lead was captured (any source)
lead.updatedFields changed (data.changes names them)
lead.status_changedNew status (data.previousStatus)
lead.qualifiedThe lead became sales-qualified
lead.assignedOwner set or changed
lead.convertedConverted (data.dealId)
lead.sla_breachedFirst-response target missed
deal.created, deal.updated, deal.stage_changed, deal.won, deal.lostDeal 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).