Orkestia
Blog
CRM

Contacts and leads

Capture people safely, deduplicate them by phone and email, move leads through your stages, assign owners, keep the timeline, and convert a lead into a user of your app

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:

  1. 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 with deduplicated: true. Send a ref whenever you have one, and retries are safe.
  2. 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.
  3. Opens a lead at stage new when the contact has no open lead in that pipeline. When there is one, it reuses it. Pass open_lead: false to record the contact without a lead.
  4. Logs one timeline entry. A message body over WhatsApp, web or email is recorded as message_in; anything else as system.

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.

InputUse
phone_e164Phone in E.164 (+ optional, spaces and dashes are removed)
wa_idWhatsApp sender id (digits, no +). It also sets the phone
emailEmail
display_nameName, for example the WhatsApp profile name
sourceWhere they came from: whatsapp, web_form, manual (default), import, or your own short slug
source_detailAttribution, such as UTM parameters or an ad referral (JSON object)
pipeline_keyWhich pipeline the lead belongs to (default default)
owner_user_uuidMember of your organization who owns a new contact or lead
body, channel, activity_kind, occurred_at, activity_dataThe timeline entry. channel defaults from source
refExternal id that makes the call idempotent
Wire your web form or message handler to call 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 toUse
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 ofcrm.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:

  • tags is a list of up to 16 short strings. Sending tags replaces the list. Lists can filter by one tag.
  • attributes is a free JSON object for anything else about the person. It is merged: send only the keys you change, and set a key to null to 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:

  • lost closes the lead. Add lost_reason and, optionally, a note.
  • converted is set only by crm.lead.convert, and a converted lead cannot move again.
  • A lost lead 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:

kindUse
note (default)A note from your team. Needs a body
message_in / message_outA message received or sent, with its channel (whatsapp, web, email, manual)
system or your own slugAnything 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

ReadWorkflowNotes
Leads for a board or a queuedata.crm.lead.listFilter by stage, open_only, owner_user_uuid, unassigned, pipeline_key, contact_uuid. Rows include the contact's name
Contactsdata.crm.contact.listFilter by owner_user_uuid, tag, source, and search (name, phone or email). Rows leave out attributes and source_detail
One contact in fulldata.crm.contact.getThe contact with attributes and source_detail, its leads, and how many timeline entries it has
A contact's timelinedata.crm.activity.listNewest 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 with identity.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. With seat (default true) Orkestia tries to give them a seat under your app's seat policy. A missing seat does not fail the conversion: you get seated: false and a seat_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.