Contacts
A contact is the person your organization attends. Cases, messages and events hang from a contact. Phone and email are optional, because a contact can come from your own app and have neither. The externalRef is your own id for the person: use it, and you never need to store a Lelemon id in your database.
The examples use this client:
Code
Create a contact
Only name is required. Sending the same externalRef twice answers 409 external_ref_conflict.
Expected: 201 with the contact:
Contactobject, 16 fields
objectenumrequiredcontactidstringrequirednamestringrequiredemailstring or nullrequiredphonestring or nullrequiredexternalRefstring or nullrequiredcompanyIdstring or nullrequiredThe company of this contact, or null when it has none.
tagsstring[]requiredfieldsobjectrequiredThe values of the contact fields this contact has, by key. Empty when it has none.
createdAtstring (date-time)requiredupdatedAtstring (date-time)requiredpresencePresenceoptionalWhether the contact is online. Present only when the organization setting presenceEnabled is on, at least one presence signal has arrived, and the key has the contacts:presence-read scope.
Presence2 fields
onlinebooleanrequiredtrue when the last active signal is less than 120 seconds old and no offline signal came after.
lastSeenAtstring or null (date-time)requiredThe last time a signal arrived. While online it is exact; once offline it can be up to 5 minutes behind. null when no signal ever arrived for this contact.
matchobject or nulloptionalOnly when you search with q: the field of the contact that matched and its value, or null when the match was the name, email, phone or externalRef. Fields marked sensitive are never searched or shown.
match2 fields
Only when you search with q: the field of the contact that matched and its value, or null when the match was the name, email, phone or externalRef. Fields marked sensitive are never searched or shown.
fieldstringoptionalvaluestringoptional
caseCountintegeroptionalOnly when you search, filter by pipeline or open cases, or sort: how many cases the contact has.
openCaseCountintegeroptionalSame as caseCount, only the open ones.
lastActivityAtstring (date-time)optionalSame as caseCount: the latest message of its cases, or its creation date.
Create or update in one call
PUT /v2/contacts/by-external-ref/{externalRef} creates the contact when the reference is new and updates it when it exists. It always answers 200 and calling it twice leaves one contact. The fields you leave out stay as they were; a new contact with no name is named after the externalRef. This is the call to make every time your system sees a person.
To open a case for a person you may not have stored yet, send contact instead of contactId to POST /v2/cases: it finds the person by externalRef or phone, or creates them, and does not change a contact that exists.
Code
Contact fields
A contact field is a piece of data you want on every person: a plan, a customer number, a birth date. You define it once for the organization and then send its value with each contact. The dashboard shows the values in the contact page, lets your team edit them there and filters the Contacts list by them.
POST /v2/contact-fields takes a key (letters, numbers and underscores, starting with a letter, fixed once created), a type and optionally a label. The types check what you send:
type | Accepts |
|---|---|
text, textarea | A string, up to 500 and 5000 characters |
email | An email address |
phone | A phone number: digits, spaces, +, -, ( and ) |
url | An http or https URL |
number | A JSON number |
date | A date as YYYY-MM-DD |
boolean | true or false |
select | One of its options (required for this type and not allowed for the others) |
An organization can have up to 100 contact fields.
Expected: 201 with the field, whose id starts with cfd_.
ContactFieldobject, 8 fields
objectenumrequiredcontact_fieldidstringrequiredkeystringrequiredlabelstringrequiredtypeenumrequiredtexttextareaemailphoneurlnumberdatebooleanselectoptionsContactFieldOptionOut[]requiredContactFieldOptionOut2 fields
valuestringrequiredlabelstringrequired
createdAtstring (date-time)requiredupdatedAtstring (date-time)required
Send the values in fields when you create a contact, update it, or use the upsert. A key that was never defined, or a value that does not fit its type, answers 400 invalid_request with param set to fields.<key>, and nothing is saved. On an update the keys you leave out stay as they were and null removes a value.
Every contact answers its values in fields, which is {} when it has none. GET /v2/contact-fields, GET /v2/contact-fields/{id}, PATCH /v2/contact-fields/{id} (the label and, for a select, its options) and DELETE /v2/contact-fields/{id} complete the resource. Deleting a field also removes its value from every contact, and it cannot be undone. Contact fields are not for secrets: they are searchable and your whole team sees them.
Find and list contacts
GET /v2/contacts lists newest first. Filter by externalRef, phone, email or updatedSince (a date-time, to sync only what changed). presence=online keeps the contacts online now and needs contacts:presence-read. GET /v2/contacts/{id} returns one.
Lists follow the pagination rules.
Search contacts
Send q to search by name, email, phone, externalRef and the values of the contact fields, ignoring case and accents. Each result says in match which field matched and its value, or null when the match was the name, email, phone or externalRef. A field the team marked sensitive in any pipeline is never searched and never shown in match, for contacts with or without cases.
pipelineId keeps the contacts that have a case in that pipeline, hasOpenCases=true the ones with an open case, and fieldKey with fieldValue the ones whose contact field has exactly that value. order is activity (latest activity first) or name. They combine with each other and with the filters above.
With any of these parameters each contact also carries caseCount, openCaseCount and lastActivityAt, and the list is no longer newest first: it follows order, which is activity when you do not send it. It is the same search as the Contacts screen of the dashboard.
Companies
A company groups the contacts that work in it. GET /v2/companies lists them by name, with q (part of the name), status and type filters, and GET /v2/companies/{id} returns one with the ids of its contacts in contactIds. Both need contacts:read.
POST /v2/companies, PATCH /v2/companies/{id} and DELETE /v2/companies/{id} need companies:write, the same permission the dashboard asks of its owners and admins to edit a company. Only name is required: type is prospect and status is lead when you do not send them. The rut is unique among the live companies of the organization, and one that is taken answers 409 conflict with param: "rut". PATCH changes only what you send, and null removes rut, website, industry, source or notes.
Delete is for data created by mistake: a company that was lost as a business stays, with status: "lost". Deleting a company never deletes its contacts. They stay, their companyId becomes null, and the rut can be used again.
To link a person to a company, send companyId when you create the contact or in PATCH /v2/contacts/{id}. null unlinks it, and a company that does not exist in the organization answers 400 with param: "companyId". Linking is a contact edit, so it needs contacts:write. Every contact answers its companyId.
companyId is also accepted by PUT /v2/contacts/by-external-ref/{externalRef} (omitted keeps the company, null unlinks), by contact in POST /v2/cases and by contact in POST /v2/contact-events, with param: "contact.companyId" when the company does not exist. In POST /v2/cases it applies only when the contact is created there. Deleting a company at the same time as linking a contact to it is safe: the contact is either linked to a live company or the link is refused.
Companyobject, 12 fields
objectenumrequiredcompanyidstringrequirednamestringrequiredrutstring or nullrequiredtypeenumrequiredWhat the company is for the organization.
prospectclientpartnervendorstatusenumrequiredHow the relationship is today.
leadactivepausedlostwebsitestring or nullrequiredindustrystring or nullrequiredsourcestring or nullrequirednotesstring or nullrequiredcreatedAtstring (date-time)requiredupdatedAtstring (date-time)required
The account in your system
If a contact has an externalRef and the organization has an active data connector, the contact page of the dashboard shows the account of that reference in your system, read live each time the page opens. It uses the same permissions and the same masking as the connector: only owners and admins see it, personal data stays hidden until someone reveals it and each reveal is recorded. Nothing is stored. When your system does not answer, the page says why and the rest of the contact keeps working.
Update a contact
PATCH /v2/contacts/{id} changes only what you send. Send null in email, phone or externalRef to remove it. tags replaces the whole list, so send every tag you want to keep.
Events about a contact
Tell Lelemon what happened to a person in your system and let it message them. POST /v2/contact-events takes a type you choose, the contact (by externalRef) and your data. A trigger that listens to that type sends the template, opens the case or moves it. The contact is created if it does not exist.
Code
The event costs money when a message goes out, so it takes ?dryRun=true and requires an Idempotency-Key. The full flow, with templates and cadences, is in Message on events.
Presence
If your product has a web or mobile app, report who is online and your team sees it in the inbox and can filter by it. See Show who is online.
Opt-outs
An opt-out stops all sending to a contact on one channel (whatsapp, email or voice): events, messages and campaigns skip them. POST /v2/whatsapp/templates/{id}/test too: a phone that belongs to a contact who opted out of whatsapp answers 422 contact_opted_out and nothing is sent. A contact who writes "stop" is opted out by the platform; you can also set it yourself. GET /v2/opt-outs lists them and needs contacts:read.
Code
DELETE clears only the opt-outs you set. One the contact asked for stays until they write again, and answers 409.
An opt-out blocks every WhatsApp message to that number, whoever sends it: a template, a reply from the team in the dashboard, an agent or a test send (422 contact_opted_out). It also holds for another contact with the same phone number or email. It survives the erasure of the person: the opt-out keeps only a keyed hash of the number and the address, never the number itself, so a new contact with the same number is still opted out.
Erase a contact
DELETE /v2/contacts/{id} erases the person for good: the contact, its cases, messages, files, fields, agent memory and logs lose their content. It runs as a job and answers 202 with a suppression you follow with GET /v2/suppressions/{id} and the contact.suppressed webhook. It answers 409 resource_in_use while the contact has open cases, unless you send closeOpenCases=true. Use ?dryRun=true first to see the inventory without content. The deadlines, what is kept and how to redact a single message are in Erase a contact or a message.
Code
Errors you will meet
| Error | Cause |
|---|---|
400 invalid_request | A field breaks a limit, or a value in fields does not fit its type; param names it |
403 scope_missing | The key lacks contacts:write or contacts:read |
404 not_found | The contact does not exist in this mode, or it was erased |
409 external_ref_conflict | Another contact already uses that externalRef |
409 conflict | A contact field with that key already exists, or the organization has 100 |
409 resource_in_use | Erasure of a contact with open cases |
All codes are in the error catalog.
Good practice
- Put your own customer id in
externalRefand use the upsert. It makes every sync safe to repeat. - Store phones in E.164 (
+56911112222). The API keeps what you send. - Do not use tags as a database: keep a few that your team filters by. For data about the person, define a contact field.
- Check opt-outs before you build your own sending on top of Lelemon; Lelemon already skips them.
What's next
- Message on events to write first.
- Pipelines for the cases a contact opens.
- Erase a contact or a message for data requests.