Lelemon
  • Get started
  • Use cases
  • Guides
  • API basics
  • Webhooks
  • SDK, CLI and MCP
  • API reference
  • Changelog
  • v1 (legacy)
Configure your account
Talk to people
    Build your own inboxWhatsApp messagesWrite by emailMessage on eventsRun a campaignSchedule meetingsShow who is online
Run and protect
powered by Zudoku
Talk to people

Build your own inbox

An inbox is a list of cases. A case is one conversation with one contact inside a pipeline.

You will

List cases, show what the agent is doing, take control and answer as your team.

You need

  • A key with cases:read and cases:send
  • A webhook endpoint for message.created
Caseobject, 24 fields
  • objectenumrequired

    case

  • idstringrequired

    Example cse_4Zq0x8bVw1TqS9mYp3RkLd

  • contactIdstringrequired

    Example ctc_4Zq0x8bVw1TqS9mYp3RkLd

  • pipelineIdstringrequired

    Example ppl_02y0e5MANqacVbmtn1YyeJ

  • stageIdstring or nullrequired
  • agentIdstring or nullrequired

    The agent that answers the case: the copilot of a copilot case, otherwise the agent of the current stage.

  • statusenumrequired

    openclosed

  • assigneeIdstring or nullrequired
  • channelenumrequired

    Where the contact talks: whatsapp, email, api (your own system) or in_app. null when the case has no channel to write back.

    whatsappapiin_appemailnull

  • phoneNumberIdstring or nullrequired

    The WhatsApp number the contact wrote to, when it is a WhatsApp case.

    Example pn_4Zq0x8bVw1TqS9mYp3RkLd

  • controlenumrequired

    Who answers the contact. human also covers a case the agent escalated and nobody took yet: see agentState.

    agenthuman

  • agentStateenumrequired

    What the agent is doing, to drive a typing indicator: thinking and replying mean it is working; waiting_for_contact means it answered; escalated means it asked for a person; idle is everything else.

    idlethinkingreplyingwaiting_for_contactescalated

  • unreadintegerrequired

    Messages of the contact since the last POST /v2/cases/{id}/read.

  • lastMessageAtstring or null (date-time)required
  • lastMessageLastMessage or nullrequired

    A preview of the last public message.

    LastMessage3 fields
    • authorenumrequired

      contactagentteam

    • typestringrequired

      Example text

    • textstring or nullrequired

      The first 140 characters. null when the message is a file.

  • contactCaseContactoptional

    The contact with its presence. Present only on GET /v2/cases and GET /v2/cases/{id}, when the organization setting presenceEnabled is on, at least one presence signal has arrived, and the key has the contacts:presence-read scope.

    CaseContact2 fields
    • idstringrequired

      Example ctc_4Zq0x8bVw1TqS9mYp3RkLd

    • presencePresencerequired
      Presence2 fields
      • onlinebooleanrequired

        true when the last active signal is less than 120 seconds old and no offline signal came after.

      • lastSeenAtstring or null (date-time)required

        The 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.

  • outcomestring or nullrequired
  • closedAtstring or null (date-time)required
  • fieldsobjectrequired

    The values of the fields the pipeline declares, by key. Sensitive fields are not included.

  • tagsstring[]required

    The tags of the contact of the case.

  • archivedAtstring or null (date-time)required

    When the case was archived: out of the board, state untouched. null when it is on the board.

  • mutedbooleanrequired

    true while the contact is muted: the agent does not answer them and their open cases are closed. Muting belongs to the person, so every case of the contact shows it.

  • createdAtstring (date-time)required
  • updatedAtstring (date-time)required

Every case points to a contact, and every message belongs to a case:

Contactobject, 16 fields
  • objectenumrequired

    contact

  • idstringrequired

    Example ctc_4Zq0x8bVw1TqS9mYp3RkLd

  • namestringrequired

    Example Ana Pérez

  • emailstring or nullrequired
  • phonestring or nullrequired
  • externalRefstring or nullrequired
  • companyIdstring or nullrequired

    The company of this contact, or null when it has none.

    Example cpy_4Zq0x8bVw1TqS9mYp3RkLd

  • tagsstring[]required
  • fieldsobjectrequired

    The values of the contact fields this contact has, by key. Empty when it has none.

    Example {"plan":"pro"}

  • createdAtstring (date-time)required
  • updatedAtstring (date-time)required
  • presencePresenceoptional

    Whether 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
    • onlinebooleanrequired

      true when the last active signal is less than 120 seconds old and no offline signal came after.

    • lastSeenAtstring or null (date-time)required

      The 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 nulloptional

    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.

    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.

    • fieldstringoptional

      Example Plan

    • valuestringoptional

      Example enterprise

  • caseCountintegeroptional

    Only when you search, filter by pipeline or open cases, or sort: how many cases the contact has.

  • openCaseCountintegeroptional

    Same as caseCount, only the open ones.

  • lastActivityAtstring (date-time)optional

    Same as caseCount: the latest message of its cases, or its creation date.

Messageobject, 18 fields
  • objectenumrequired

    message

  • idstringrequired

    Example msg_4Zq0x8bVw1TqS9mYp3RkLd

  • caseIdstringrequired

    Example cse_4Zq0x8bVw1TqS9mYp3RkLd

  • authorenumrequired

    contactagentteam

  • authorNamestring or nullrequired
  • visibilityenumrequired

    publicinternal

  • textstringrequired
  • contentTypestringrequired

    Example text

  • attachmentsMessageAttachment[]required
    MessageAttachment4 fields
    • typestringrequired

      Example image

    • mimeTypestringrequired

      Example image/png

    • urlstringrequired

      A signed link. Open it before expiresAt.

    • expiresAtstring (date-time)required
  • interactiveInteractive or nulloptional

    The buttons, list, product or catalog the message carried. null on any other message.

    InteractiveButtonsMessage6 fields
    • typestringrequired
    • bodystringrequired

      The text of the message. Required for buttons, list and product_list.

    • headerstringoptional

      Text above the body. Required for product_list.

    • footerstringoptional
    • headerMediaInteractiveMediaHeaderoptional

      An image, video or document above the body. Only for buttons and cta_url. Do not send it together with header.

      InteractiveMediaHeader2 fields
      • typeenumrequired

        imagevideodocument

      • urlstringrequired

        A public https link WhatsApp can download.

        Example https://cdn.example.com/order.jpg

    • buttonsInteractiveButton[]required

      Only for type buttons: 1 to 3 buttons.

      InteractiveButton2 fields
      • idstringrequired

        Comes back as reply.id when the person taps it. Unique within the message.

        Example appt:1:confirm

      • titlestringrequired

        Example Confirm

  • replyMessageReply or nulloptional

    Set when the person tapped a button or a list row: the id and title they tapped. null otherwise.

    MessageReply2 fields
    • idstringrequired

      Example appt:1:confirm

    • titlestringrequired

      Example Confirm

  • orderMessageOrder or nulloptional

    Set when the person sent a cart from the WhatsApp catalog. text carries their note. null otherwise.

    MessageOrder2 fields
    • catalogIdstringrequired

      Example 1234567890

    • itemsMessageOrderItem[]required
      MessageOrderItem4 fields
      • productIdstringrequired

        Example sku-1

      • quantitynumberrequired

        Example 2

      • pricenumberrequired

        Unit price, as the catalog has it.

        Example 4990

      • currencystringrequired

        Example CLP

  • locationMessageLocation or nulloptional

    Set when the person shared a location (answering a location_request or on their own). text carries it as a sentence the agent reads. null otherwise.

    MessageLocation4 fields
    • latitudenumberrequired

      Example -33.4489

    • longitudenumberrequired

      Example -70.6693

    • namestringoptional

      Example Home

    • addressstringoptional

      Example Av. Siempre Viva 742

  • flowResponseMessageFlowResponse or nulloptional

    Set when the person finished a WhatsApp Flow: the fields they filled in. null otherwise.

    MessageFlowResponse2 fields
    • tokenstringoptional

      The token you sent with the Flow, if any.

      Example case-42

    • dataobjectrequired

      The fields the person filled in the last screen of the Flow.

      Example {"date":"2026-11-02","time":"10:00"}

  • contactMessageContact or nulloptional

    Set when the person shared their phone answering a contact_request. A contact card the person forwards on their own is not kept. null otherwise.

    MessageContact1 field
    • phonesMessageSharedPhone[]required
      MessageSharedPhone2 fields
      • phonestringrequired

        Example +56912345678

      • waIdstringoptional

        Example 56912345678

  • errorMessageError or nulloptional

    Set when status is failed: why. null on any other status. Read it to know whether to retry, send a template (outside the 24-hour window) or stop.

    MessageError2 fields
    • codenumber or nullrequired

      The WhatsApp error code when WhatsApp rejected the message. null when the platform stopped it before sending.

      Example 131026

    • messagestringrequired

      Why the message failed.

      Example Message undeliverable

  • statusenumrequired

    Delivery to the channel of the case.

    pendingsentdeliveredreadfailed

  • createdAtstring (date-time)required

List what needs attention

Keep the list fresh with the message.created, case.control_changed and case.escalated webhooks instead of polling.

Show what the agent is doing

agentState on the case drives a typing indicator.

agentStateMeaning
thinking, replyingThe agent is working
waiting_for_contactThe agent answered
escalatedThe agent asked for a person
idleAnything else

control says who answers the contact: agent or human. human also covers a case the agent escalated that nobody took yet.

Let a person answer

Take control, write as the team, and hand the case back.

Writing as team needs cases:send. While the agent is in control the call answers 409 case_agent_in_control, so a contact never gets two answers at once. On WhatsApp, free text more than 24 hours after the contact's last message answers 422 channel_window_closed; send a template instead.

Send buttons, lists and products

On a WhatsApp case, interactive replaces text. The person answers with one tap, and the tap arrives as a message of the contact with reply: { id, title }.

typeContent fieldLimits
buttonsbuttons1 to 3 buttons, titles up to 20 characters
listlistup to 10 sections and 10 rows in total, titles up to 24 characters
productproductone product of the catalog connected to the number
product_listproductsup to 30 products in up to 10 sections, with header and body
catalogcatalogopens the whole catalog
cta_urlctaone button that opens an https link
flowflowa button that opens a WhatsApp Flow
location_requestnoneasks the person to share a location
contact_requestnoneasks the person to share their phone
callcalla button that starts a WhatsApp call
carouselcarousel2 to 10 swipeable cards with an image or video

The catalog is connected by the business in Meta Commerce Manager and WhatsApp Manager, not through the API. Save its id on the number with catalogId: a product, product_list or catalog message answers 422 catalog_not_connected when the number has no catalog or the message names another one. productId is the id of the product in that catalog. The WhatsApp guide has the limits of each format and the commerce settings.

A message that breaks a limit answers 400 invalid_request with param set to the field, for example interactive.buttons. Send interactive or text, never both. Interactive messages follow the same rules as free text: cases:send, a person in control and the 24 hours window. On a case that is not on WhatsApp the answer is 422 interactive_unsupported_channel.

When the person sends a cart from the catalog, it arrives as a message with order: { catalogId, items }, and message.received carries type: "order". A shared location arrives as location, the answer of a Flow as flowResponse and a phone shared for a contact_request as contact; the event carries only their type (location, flow_response, contact) and you read the data in the message.

A message whose status is failed has error: { code, message } with the reason: WhatsApp's code, or null when the platform stopped it before sending.

Leave a private note

A message with "visibility": "internal" is a note only your team sees. It never reaches the contact or the agent and needs only cases:write.

A note signs with the name of the API key. Send authorName to sign it with the person who wrote it:

A message from the contact ("author": "contact") in a stage without an agent is saved when a person has taken control of the case. If nobody has, it answers 422, because nobody would answer. POST /v2/cases with only agentId answers 422 on pipelineId when that agent attends the entry stage of more than one pipeline: send pipelineId or stageId too. case.control_changed carries contactId and externalRef.

Tag, fill and reveal

tags and fields change with PATCH /v2/cases/{id}. Tags replace the whole list and belong to the contact, so the contact and its other cases show the same ones. fields writes the fields the pipeline declares, whoever fills them (the team, the channel, your system or the agent). A key the pipeline does not declare answers 400 invalid_request with param: "fields" and nothing is saved.

A field marked sensitive is written but never read back: the case, the webhooks and the agent do not carry it. To see it, reveal it with a key that has cases:reveal, a scope of its own that cases:read and cases:write do not include.

Every reveal writes who looked, the case and the field into an audit trail before the value is returned. The trail never holds the value. The response is not kept for an idempotent replay and not in the request log, so a retry reveals again and is audited again. A field that is not sensitive, or that the pipeline does not declare, answers 422 field_not_sensitive. It is the same audit trail the dashboard writes to when a person reveals a field.

Pin a note

PATCH /v2/cases/{id}/notes/{noteId} takes pinned. A case has one pinned note, so pinning one unpins the one that was pinned. Send content, pinned or both.

Archive, move and change pipeline

archive takes a case off the board without deleting it or changing its status, and unarchive brings it back. Both are safe to repeat. The case carries archivedAt, and GET /v2/cases?archived=true lists the archived ones.

move takes stageId or pipelineId, never both. With pipelineId the case goes to the entry stage of that pipeline, with the same checks as a move to a stage of another pipeline.

POST /v2/cases/move-batch moves up to 100 cases of one pipeline to a stage of that pipeline. It does not fail as a whole: the ones that can move move, and notMoved gives the reason for each of the others.

reasonMeaning
not_foundNo such case in your organization
not_in_pipelineThe case is in another pipeline. Use move with pipelineId
archived, closedThe case cannot move
already_in_stageNothing to do
required_fields_missingThe stage needs fields the case lacks. missingFields has their keys

Mute and hand to a person

mute silences the contact of the case: the agent stops answering and every open case of the contact closes. Muting belongs to the contact, so every case of the person shows muted: true until unmute. The cases it closed stay closed.

escalate marks the case as needing a person, with a reason your team reads. It fires case.control_changed with escalated: true, and the case answers control: "human". case.escalated is the event of an agent handing a case over, so it does not fire here. A closed case answers 409.

Read the history of a person

The cases of a person are GET /v2/cases?contactId=ctc_..., and the messages of each one are in GET /v2/cases/{id}/messages. The events you reported for the person are GET /v2/contacts/{id}/events, newest first, with what each one sent or why it did not.

Remind your team

A reminder is a task for a member, due at a time. The assignee sees it in their tasks in the dashboard and gets the notice when it is due, like one a person created there.

GET /v2/cases/{id}/reminders?status=active lists the ones still to do. assigneeId has to be an active member (400 on assigneeId otherwise). PATCH /v2/reminders/{id} edits it, dismiss drops it but keeps it in the list, and DELETE removes it. Creating and editing need cases:write, reading needs cases:read.

Other case actions

move changes the stage or the pipeline, assign sets the assignee (take the mbr_ id from GET /v2/members, which needs members:read), close and reopen change the status, and read clears unread. Attachments are uploaded to /v2/cases/{id}/attachments and sent by passing attachmentIds in a message.

API keysWhatsApp messages
On this page
  • List what needs attention
  • Show what the agent is doing
  • Let a person answer
  • Send buttons, lists and products
  • Leave a private note
  • Tag, fill and reveal
  • Pin a note
  • Archive, move and change pipeline
  • Mute and hand to a person
  • Read the history of a person
  • Remind your team
  • Other case actions
curl "https://api.lelemon.cl/v2/cases?status=open" \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl -X POST https://api.lelemon.cl/v2/cases/cse_.../take-control \ -H "Authorization: Bearer $LELEMON_API_KEY" curl https://api.lelemon.cl/v2/cases/cse_.../messages \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: reply-cse-0043" \ --json '{ "author": "team", "authorName": "Camila", "text": "Perdón, te devolvemos el envío. ¿Te sirve?" }' curl -X POST https://api.lelemon.cl/v2/cases/cse_.../release-control \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl https://api.lelemon.cl/v2/cases/cse_.../messages \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: confirm-cse-0043" \ --json '{ "author": "team", "interactive": { "type": "buttons", "body": "Do you confirm your appointment tomorrow at 10:00?", "buttons": [ { "id": "appt-43-confirm", "title": "Confirm" }, { "id": "appt-43-cancel", "title": "Cancel" } ] } }'
curl https://api.lelemon.cl/v2/cases/cse_.../messages \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "author": "team", "visibility": "internal", "text": "Frequent customer, offer a free drink." }'
curl https://api.lelemon.cl/v2/cases/cse_.../notes \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "content": "Called, no answer.", "authorName": "Ana Pérez" }'
curl -X PATCH https://api.lelemon.cl/v2/cases/cse_... \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "tags": ["vip", "rm"], "fields": { "bank_account": "000123456" } }'
curl -X POST https://api.lelemon.cl/v2/cases/cse_.../fields/bank_account/reveal \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl -X PATCH https://api.lelemon.cl/v2/cases/cse_.../notes/not_... \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "pinned": true }'
curl https://api.lelemon.cl/v2/cases/move-batch \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: batch-2026-11-01" \ --json '{ "caseIds": ["cse_...", "cse_..."], "stageId": "stg_..." }'
curl https://api.lelemon.cl/v2/cases/cse_.../escalate \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "reason": "The person asked to talk to someone" }' curl https://api.lelemon.cl/v2/cases/cse_.../mute \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "reason": "Sends spam" }'
curl https://api.lelemon.cl/v2/cases/cse_.../reminders \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "title": "Call back about the quote", "type": "call", "priority": "high", "dueAt": "2030-01-15T14:00:00.000Z", "assigneeId": "mbr_..." }' curl https://api.lelemon.cl/v2/reminders/rem_.../complete \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "completionNote": "Talked to them." }'