Contacts and leads
Every call on this page needs identity_app_uuid, the Identity app whose CRM you are working in (identity.app.query lists your apps). Your organization comes from your token; you never pass it.
Capture someone who reached you
crm.lead.capture is the entry point for a new inbound person: a WhatsApp message, a web form, an email, or a call your team logs by hand. In one step it:
- Checks
ref. When the external id you pass (a message id, a form submission id) is already on the app's timeline, nothing is written. You get the earlier contact, lead and timeline entry back withdeduplicated: true. Send arefwhenever you have one, and retries are safe. - Finds or creates the contact. It matches the exact phone first, then the email (case-insensitive). On a match it only fills fields that are empty. It never takes a phone or email that another contact already has.
- Opens a lead at stage
newwhen the contact has no open lead in that pipeline. When there is one, it reuses it. Passopen_lead: falseto record the contact without a lead. - Logs one timeline entry. A message
bodyover WhatsApp, web or email is recorded asmessage_in; anything else assystem.
You need at least one of phone_e164, wa_id or email.
curl -sS -X POST https://workflow-api.orkestia.dev/api/workflows/start \
-H "Authorization: Bearer $ORKESTIA_TOKEN" -H "Content-Type: application/json" \
-d '{
"workflow_type": "crm.lead.capture",
"initial_data": {
"identity_app_uuid": "…",
"phone_e164": "+5511999998888",
"display_name": "Ana Souza",
"source": "web_form",
"source_detail": { "utm_source": "newsletter" },
"body": "I would like a quote for 20 seats",
"ref": "form-submission-8812"
}
}'
The run returns contact_uuid, lead_uuid, activity_uuid, stage, contact_created, lead_created, deduplicated and opted_out.
| Input | Use |
|---|---|
phone_e164 | Phone in E.164 (+ optional, spaces and dashes are removed) |
wa_id | WhatsApp sender id (digits, no +). It also sets the phone |
email | |
display_name | Name, for example the WhatsApp profile name |
source | Where they came from: whatsapp, web_form, manual (default), import, or your own short slug |
source_detail | Attribution, such as UTM parameters or an ad referral (JSON object) |
pipeline_key | Which pipeline the lead belongs to (default default) |
owner_user_uuid | Member of your organization who owns a new contact or lead |
body, channel, activity_kind, occurred_at, activity_data | The timeline entry. channel defaults from source |
ref | External id that makes the call idempotent |
crm.lead.capture from your server with an organization token, and pass the form or message id as ref. Never put an organization token in a browser.Add or edit contacts
| You want to | Use |
|---|---|
| Add a contact, or update the one with the same phone or email (imports, manual entry) | crm.contact.upsert |
| Edit one contact you already have the id of | crm.contact.update |
crm.contact.upsert uses the same matching as capture. On a match, the name, owner and tags you send replace the current ones, attributes are merged, and phone, email and WhatsApp id only fill empty fields. It opens no lead and writes no timeline entry.
crm.contact.update changes only the fields you send. clear_fields empties display_name, phone_e164, email, wa_id or owner_user_uuid. A contact must keep a phone or an email, and a phone or email another contact already has is refused. The result lists changed_fields.
Tags and attributes:
tagsis a list of up to 16 short strings. Sendingtagsreplaces the list. Lists can filter by one tag.attributesis a free JSON object for anything else about the person. It is merged: send only the keys you change, and set a key tonullto remove it. Lists do not filter on attributes.
Pipelines and stages
A lead belongs to one pipeline (pipeline_key, default default). A contact has at most one open lead per pipeline. Use separate pipelines when the same person can be in two processes at once (for example sales and support).
Leads start at new. The suggested path is new → contacted → qualified → converted or lost, but a stage is any short lowercase slug, so you can use your own (demo, proposal, negotiation).
crm.lead.update-stage moves a lead and logs a stage_change entry on the timeline:
lostcloses the lead. Addlost_reasonand, optionally, anote.convertedis set only bycrm.lead.convert, and a converted lead cannot move again.- A
lostlead can be reopened only while the contact has no other open lead in that pipeline. - Setting the stage the lead already has changes nothing (
changed: false).
A lead is open in any stage except converted and lost.
Owners
crm.lead.assign gives a lead to a member of your organization (owner_user_uuid), or clears it with unassign: true. It logs an assignment entry. The contact gets the same owner when it has none, unless you pass also_contact: false. Members are listed by identity.membership.query.
The timeline
Every change above already writes to the timeline. Add your own entries with crm.activity.add:
kind | Use |
|---|---|
note (default) | A note from your team. Needs a body |
message_in / message_out | A message received or sent, with its channel (whatsapp, web, email, manual) |
system or your own slug | Anything else, for example call or task_done |
stage_change, assignment and conversion are written only by the workflows that make those changes, so the history stays trustworthy. Pass lead_uuid to tie an entry to one of the contact's leads, and ref to make it idempotent. Each entry records who made it: a person, a Staff agent, or the system.
Read the CRM
| Read | Workflow | Notes |
|---|---|---|
| Leads for a board or a queue | data.crm.lead.list | Filter by stage, open_only, owner_user_uuid, unassigned, pipeline_key, contact_uuid. Rows include the contact's name |
| Contacts | data.crm.contact.list | Filter by owner_user_uuid, tag, source, and search (name, phone or email). Rows leave out attributes and source_detail |
| One contact in full | data.crm.contact.get | The contact with attributes and source_detail, its leads, and how many timeline entries it has |
| A contact's timeline | data.crm.activity.list | Newest first, optionally by lead_uuid or kind. Bodies are cut at 2,000 characters (body_truncated) |
Lead and contact lists put the most recent activity first. Every list pages with limit and offset and returns total and has_more.
Turn a lead into a user of your app
crm.lead.convert makes an open lead an end user of your app. It needs an organization owner or admin.
- Link an existing end user. Pass
end_user_uuid. The end user must belong to the same app. This works for people with only an email: create the end user first withidentity.end-user.create, then convert with its id. - Create one from the phone. Without
end_user_uuid, the contact's phone becomes a new end user of your app. Withseat(defaulttrue) Orkestia tries to give them a seat under your app's seat policy. A missing seat does not fail the conversion: you getseated: falseand aseat_reason.
Either way the lead moves to converted, the contact is linked to the end user, and a conversion entry is logged. Running the same conversion again returns the recorded result. A contact with no phone needs end_user_uuid.
Use it from a composition
The CRM workflows also work as steps of a composition, so you can chain them with the rest of the catalog, for example capture a lead and then add a follow-up note. CRM data is for your organization's members, so do not expose a composition with CRM steps to your app's end users.
CRM
Orkestia's built-in CRM keeps the people who reach your app as contacts, tracks each one as a lead through your stages, and records every touch on one timeline, through twelve crm workflows you can call directly
Privacy and erasure
Who can see CRM data, how to record an opt-out, and how to erase a person's data on request while keeping your counts and funnel history
