Lelemon
  • Get started
  • Use cases
  • Guides
  • API basics
  • Webhooks
  • SDK, CLI and MCP
  • API reference
  • Changelog
  • v1 (legacy)
WhatsApp sales agentMeeting schedulingSupport with approvalsInternal copilot with a data connectorPlatform with subaccountsBackground runs
powered by Zudoku
Use cases

Sell on WhatsApp with a catalog and a cart

You will

Run a WhatsApp sales agent end to end: qualify, show catalog products, take a cart, hand over to a person and send a campaign.

You need

  • An API key with the scopes in this page
  • Node.js 20 or later for the TypeScript steps
  • The id of your Meta catalog (connected in WhatsApp Manager)

A store gets a WhatsApp message, the agent greets, asks what the person is looking for, shows products from the catalog, receives the cart the person builds, and tells the team to close the sale. When the person asks for a human, the case moves to a stage where your team answers. Weeks later, a template campaign brings quiet contacts back.

Who it is for. Stores and service businesses that sell from a catalog and close the sale with a person. Scopes. agents:write, pipelines:write, knowledge:write, skills:write, actions:write, webhooks:write, whatsapp:write, cases:read, cases:write, cases:converse, turns:read, contacts:write, files:write is not needed. Use a key with all of them, or the one the setup guide creates.

Code
Customer (WhatsApp) Lelemon Your backend / team | "busco una lámpara" -> agent qualifies, saves fields | check_stock ------------------> GET /stock (signed) | <- product message send_products (your catalog) | cart (order) ----------> message.received type=order ---> create the order, PATCH case, move stage | "quiero una persona" -> transfer_to_human --------------> case.escalated, stage "Needs a person" | team replies from your inbox Template campaign <------ campaign to your contacts <---- you launch it

The runnable version of every step is in examples/use-cases/whatsapp-sales-agent. The TypeScript blocks of this page come from that script, which ran against a real number. It stops before the campaign, because that step messages people.

0. Set up

Every example reads the key from LELEMON_API_KEY and the TypeScript steps use the SDK. The cURL tabs send the same requests.

Code
import { Lelemon } from '@lelemon-ai/sdk'; const lelemon = new Lelemon({ apiKey: process.env.LELEMON_API_KEY });

1. Write the agent

The prompt is the product. This one gives the agent a role, a voice, the order in which it asks for data, how it uses the catalog, hard limits and exactly when to hand over. The catalog id and the retailer ids go in the prompt: send_products takes them from there, so the agent never invents one.

The agent starts as a draft. The channels say where it can answer: whatsapp for the number, api for cases you open yourself. A low temperature keeps prices and policies stable. See Agents for every field.

What you get:

Code
agent {"id":"agt_4zhi3Ep39ipRoNmOqg4DJS","status":"draft","version":1}

2. Create the pipeline and its stages

A pipeline is the path of a sale. The entry stage is where every new case starts and where the agent works. Needs a person is where a handed-over case lands, and the next steps use stageId to refer to a stage by its name.

What you get:

Code
pipeline {"id":"ppl_18UEE9xEomyeUh4R879h7v","stages":["Qualifying (entry)","Proposal","Order placed","Needs a person","Closed"]}

3. Create the fields of the case

Fields are the data the sale needs. source: "agent" means the agent fills them with save_case_data while it talks; source: "system" means only your backend writes them. A stage can require fields before a case enters it: Order placed needs the comuna and the order reference, so a case cannot jump there half empty.

What you get:

Code
fields ["interest (agent)","budget_clp (agent)","purchase_timeframe (agent)","delivery_comuna (agent)","order_reference (system)"]

4. Give the agent knowledge

Policies that change and are too long for a prompt go in a knowledge collection. The agent searches only the collections you grant it.

A document starts as processing. Listen for knowledge_document.ready or knowledge_document.failed before you count on it. See Knowledge.

5. Write the skills

A skill is a procedure the agent opens only when the conversation needs it. Only its name and description travel in the prompt; the steps are loaded on demand, which keeps the prompt short.

There is no tool to grant for skills. As soon as an agent has one, Lelemon gives it the tool that opens them on every turn, and GET /v2/agents/{id}/skills is the list of what it can open.

6. Register the stock check as an action

An action is an HTTP endpoint of yours that the agent calls as a tool. This one answers how many units are in stock. Lelemon signs every call; your server verifies it with the signing secret of the action (Actions and MCP). audience: "customers" lets an agent that talks to the public use it.

origin in the answer is the site your key must be allowed to call. If it is not, the call answers 403 action_origin_not_allowed and an administrator adds the site to the key.

7. Grant the tools

A tool does nothing until you grant it to the agent. save_case_data limits which fields the agent may write. transfer_to_human takes the stage the case moves to and the goodbye the contact reads, in and out of business hours. The goodbye is written by the tool, not the model, so the agent cannot promise what the team will not do.

What you get:

Code
granted ["check_stock","send_products","send_buttons","save_case_data","transfer_to_human"]

8. Receive events

Register an endpoint for what your backend must react to: message.received (it carries the type, order for a cart, never the text), case.escalated and case.stage_entered. Your server verifies each delivery as in Verify signatures.

9. Activate and connect the number

activate turns the agent on and says in readiness.missing what still keeps people from reaching it. An empty list means it is reachable.

setupUrl opens the Meta onboarding for the number you pass in phone; the number appears in GET /v2/whatsapp/phone-numbers when the owner finishes, and phone_number.connected fires. The script does not connect a number: it reads the id of one you already connected from LELEMON_WHATSAPP_PHONE_NUMBER_ID.

10. Connect the catalog

The catalog is created in Meta Commerce Manager and connected in WhatsApp Manager. Save its id on the number, and turn on the cart button and the catalog icon. A product message only goes out when the catalog_id of the message is the one saved here; otherwise it answers 422 catalog_not_connected.

What you get:

Code
catalog {"catalogId":"1234567890123456","cart":true,"icon":true}

11. A customer writes first

The customer writes to the number from WhatsApp. The agent answers and your webhook receives message.received.

Code
const [kase] = (await lelemon.cases.list({ query: { limit: 1 } })).data; const caseId = kase!.id; const thread = await lelemon.caseMessages.list(caseId, { query: { order: 'asc' } }); const turns = await lelemon.caseTurns.list(caseId);

The agent answers in a few seconds. case.turn_completed tells you the turn ended; then read the thread and the turn. The turn shows which tools ran, with their input and output:

Code
webhook {"type":"message.received","data":{"case":{"id":"cse_5OTq2YJhBq1dTWenpJ9KNm","stageId":"stg_1Hbz5rXer2I1d6KylWuscW","pipelineId":"ppl_18UEE9xEomyeUh4R879h7v"},"message":{"id":"msg_0K2n6DTeNPo9J6Fz3ZSRIT","type":"text","order":null,"reply":null,"channel":"whatsapp","createdAt":"2026-11-01T03:20:22.112Z","contentType":"text"}}} contact Hola! Busco una lámpara de pie para el living, algo negro y simple agent [product] agent ¡Hola! Te mostré la lámpara Arco de metal negro, en stock. Si te gusta, puedes agregarla al carrito y enviarlo por aquí. ¿Cuál es tu presupuesto aproximado en pesos? turn [{"status":"completed","steps":[{"tool":"save_case_data","input":{"datos":{"interest":"lamps"}},"output":"{\"ok\":true,\"data\":{\"guardados\":[\"interest\"],\"faltan\":[{\"clave\":\"budget_clp\",\"nombre\":\"Budget (CLP)\"},{\"clave\":\"purchase_timeframe\",\"nombre\":\"When they need it\"},{\"clave\":\"delivery_comuna\",\"nombre\":\"Delivery comuna\"}]},\"hint\":\"Lo que falta pídelo junto, en un mensaje, no de a un campo.\"}","status":"ok","durationMs":18},{"tool":"check_stock","input":{"sku":"LAMP-ARCO-01"},"output":"{\"sku\":\"LAMP-ARCO-01\",\"available\":7,\"priceClp\":59990}","status":"ok","durationMs":20},{"tool":"send_products","input":{"catalog_id":"1234567890123456","product_ids":["LAMP-ARCO-01"]},"output":"{\"ok\":true,\"data\":{\"sent\":true,\"type\":\"product\"},\"hint\":\"The person already sees it. Do not repeat the options in your text; add at most one short sentence.\"}","status":"ok","durationMs":23}]}] case {"stage":"stg_1Hbz5rXer2I1d6KylWuscW","control":"agent","state":"waiting_for_contact","fields":{"interest":"lamps"}}

The agent saved interest, checked the stock with the signed call to your server, and sent a product message from the catalog. Its wording changes from run to run; the tools it calls do not.

12. The customer sends a cart

When the person fills the cart from the catalog and sends it, the message has order: { catalogId, items }. The webhook says type: "order"; read the items from the message. Your backend creates the order in its own system, writes the reference in the field with source: "system" and moves the case.

If the case lacks a required field, move answers 422 required_fields_missing with the field in param. Here the agent already saved delivery_comuna from the message, and your backend wrote order_reference, so the move succeeds:

Code
webhook {"type":"message.received","message":{"id":"msg_1dU63m9IyYEib3OLZA2oXe","type":"order","order":{"items":[{"price":59990,"currency":"CLP","quantity":1,"productId":"LAMP-ARCO-01"}],"catalogId":"1234567890123456"},"reply":null,"channel":"whatsapp","createdAt":"2026-11-01T03:20:38.687Z","contentType":"text"}} order {"text":"Lo quiero. Lo despachan a Providencia.","items":[{"price":59990,"currency":"CLP","quantity":1,"productId":"LAMP-ARCO-01"}],"catalogId":"1234567890123456"} agent said Perfecto, anoté la lámpara Arco negra ($59.990, 7 unidades disponibles) con despacho a Providencia. Un miembro del equipo confirmará el pago y la entrega. Para dejar el pedido listo, ¿me confirmas en un solo mensaje? - Tu presupuesto aproximado en pesos - Cuándo la necesitas: esta semana, este mes o solo estás explorando case {"stage":"stg_7E4Jbz9mM3JPbMp7QcWWVM","fields":{"interest":"lamps","delivery_comuna":"Providencia","order_reference":"ORD-1042"}}

13. The customer asks for a person

The prompt lists when to hand over: a human requested, a discount, a bulk order. The agent calls transfer_to_human. The case goes to human control and to Needs a person, the contact reads the goodbye of the tool and case.escalated fires with the history.

A person writes with author: "team" while the case is in human control; it needs the 24-hour window of WhatsApp. release-control hands the case back to the agent, which then answers with the messages of the team in its context.

Code
webhook {"type":"case.escalated","data":{"case":{"id":"cse_5OTq2YJhBq1dTWenpJ9KNm","channel":"whatsapp","pipelineId":"ppl_18UEE9xEomyeUh4R879h7v"},"contact":{"id":"ctc_0jEmUaPPMxXksKqHiqe7v9","name":"Ana Rojas","email":null,"phone":"56911112222","externalRef":null},"history":[{"role":"user","content":"Hola! Busco una lámpara de pie para el living, algo negro y simple","createdAt":"2026-11-01T03:20:22.112Z"},{"role":"assistant","content":"[product]","createdAt":"2026-11-01T03:20:36.471Z"},{"r … case {"stage":"stg_6K37FCS5PSfRcoj6Sbe5g1","control":"human","state":"escalated"} team message {"id":"msg_5V9k59ptqoyJuIo2C21KVb","status":"pending"}

14. Re-engage contacts with a template campaign

Outside the 24-hour window only an approved template goes out. Create it, wait for approved (WhatsApp reviews it), then send a campaign to a list of contacts. onReply names the agent that continues when someone answers.

The preview shows who gets the message, who is skipped and why, and a rendered sample before anything goes out. Launching freezes the list. See Run a campaign.

Code
template {"id":"tpl_0Jey2724PwoDA0iksK3Kp5","status":"in_review"} template {"id":"tpl_0Jey2724PwoDA0iksK3Kp5","status":"approved","variables":["name","coupon"]} preview {"recipients":2,"skipped":{},"sample":"Hola Luis Soto, llegó la colección de primavera a Casa Almendra. Usa el código PRIMAVERA10 para un 10% de descuento esta semana."} campaign {"status":"completed","counts":{"recipients":2,"pending":0,"queued":0,"sent":0,"delivered":2,"read":0,"replied":0,"failed":0,"skipped":0}} recipients ["delivered","delivered"]

Notes

  • A wrong catalog id shows up as a failed message.
  • Past the 24-hour window only an approved template goes out.

What you see in the dashboard

  • Inbox: the case of Ana with the thread, the product message, the cart and who answered (agent or team). The fields interest, delivery_comuna and order_reference show in the card.
  • Pipeline board: the case moves from Qualifying to Order placed, and a handed-over case waits in Needs a person.
  • Agent, Activity: each turn with the tools it called, tokens and cost.
  • WhatsApp, Campaigns: the recipients of the campaign with their status.

Before customers write

  1. Save the real catalog id on the number.
  2. Turn on the recipient allowlist while you validate.

What's next

  • Schedule meetings from the same conversation.
  • Approve agent actions before an agent moves money.
  • WhatsApp messages: every format and what the person can send back.
  • API reference: POST /v2/agents, POST /v2/pipelines, PUT /v2/agents/{id}/tools/{toolId}, POST /v2/campaigns.
Meeting scheduling
On this page
  • 0. Set up
  • 1. Write the agent
  • 2. Create the pipeline and its stages
  • 3. Create the fields of the case
  • 4. Give the agent knowledge
  • 5. Write the skills
  • 6. Register the stock check as an action
  • 7. Grant the tools
  • 8. Receive events
  • 9. Activate and connect the number
  • 10. Connect the catalog
  • 11. A customer writes first
  • 12. The customer sends a cart
  • 13. The customer asks for a person
  • 14. Re-engage contacts with a template campaign
  • Notes
  • What you see in the dashboard
  • Before customers write
  • What's next
TypeScript
curl https://api.lelemon.cl/v2/agents \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: almendra-agent-1" \ --json '{ "name": "Almendra", "description": "Sales assistant of Casa Almendra on WhatsApp", "instructions": "$(cat instructions.md)", "llmConfig": { "model": "anthropic/claude-sonnet-5.5", "temperature": 0.3 }, "channels": ["whatsapp", "api"], "behaviors": { "displayName": "Almendra" } }'
curl https://api.lelemon.cl/v2/pipelines \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: sales-pipeline-1" \ --json '{ "name": "WhatsApp sales", "description": "From the first message to a confirmed order", "stages": [ { "name": "Qualifying", "entry": true, "agentId": "agt_..." }, { "name": "Proposal" }, { "name": "Order placed" }, { "name": "Needs a person" }, { "name": "Closed" } ] }'
curl https://api.lelemon.cl/v2/pipelines/ppl_.../fields \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: field-interest-1" \ --json '{ "key": "interest", "type": "select", "label": "Interest", "source": "agent", "instruction": "What the person wants to buy. Pick the closest option.", "options": [ { "value": "lamps", "label": "Lamps" }, { "value": "ceramics", "label": "Ceramics" }, { "value": "textiles", "label": "Textiles" }, { "value": "furniture", "label": "Small furniture" }, { "value": "other", "label": "Something else" } ] }' curl -X PATCH https://api.lelemon.cl/v2/pipelines/ppl_.../stages/stg_... \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "requiredFields": ["delivery_comuna", "order_reference"] }'
curl https://api.lelemon.cl/v2/knowledge-collections \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: kb-policies-1" \ --json '{ "name": "Casa Almendra policies", "description": "Shipping, returns and payment" }' curl -X PUT https://api.lelemon.cl/v2/agents/agt_.../knowledge-collections/kbc_... \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl https://api.lelemon.cl/v2/agents/agt_.../skills \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: skill-handle-order-1" \ --json '{ "name": "Handle an order", "slug": "handle-order", "description": "Use when the person sends a cart or says they want to buy.", "content": "# Handle an order\n\n1. Repeat the items and quantities of the cart in one message." }'
curl https://api.lelemon.cl/v2/actions \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: action-check-stock-1" \ --json '{ "name": "check_stock", "description": "Check how many units of a product are in stock. Use it before showing a product.", "audience": "customers", "request": { "method": "GET", "url": "https://shop.example.com/stock" }, "input": { "type": "object", "required": ["sku"], "properties": { "sku": { "type": "string", "description": "The retailer_id of the product." } } } }'
curl -X PUT https://api.lelemon.cl/v2/agents/agt_.../tools/transfer_to_human \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "config": { "stageId": "stg_...", "businessHours": { "timezone": "America/Santiago", "startHour": 9, "endHour": 19, "activeSaturday": true, "activeSunday": false }, "farewell": { "duringBusinessHours": "Te paso con una persona del equipo. Te escribe en unos minutos.", "outsideBusinessHours": "Te paso con una persona del equipo. Hoy ya cerramos: te escribe mañana desde las 9." } } }'
curl https://api.lelemon.cl/v2/webhook-endpoints \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: webhook-sales-1" \ --json '{ "url": "https://example.com/lelemon", "description": "Sales events", "enabledEvents": ["message.received", "message.created", "case.stage_entered", "case.escalated", "case.turn_completed", "phone_number.connected"] }'
curl -X POST https://api.lelemon.cl/v2/agents/agt_.../activate \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl -X PATCH https://api.lelemon.cl/v2/whatsapp/phone-numbers/pn_... \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "catalogId": "1234567890123456" }' curl -X PATCH https://api.lelemon.cl/v2/whatsapp/phone-numbers/pn_.../commerce-settings \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "isCartEnabled": true, "isCatalogVisible": true }'
TypeScript
curl -X PATCH https://api.lelemon.cl/v2/cases/cse_... \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "fields": { "order_reference": "ORD-1042" } }' curl https://api.lelemon.cl/v2/cases/cse_.../move \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: move-order-1" \ --json '{ "stageId": "stg_..." }'
curl https://api.lelemon.cl/v2/cases/cse_.../messages \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: team-reply-1" \ --json '{ "author": "team", "authorName": "Camila", "text": "Hola Ana, soy Camila del equipo. Te puedo hacer un precio especial por 12 unidades." }' curl -X POST https://api.lelemon.cl/v2/cases/cse_.../release-control \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl https://api.lelemon.cl/v2/whatsapp/templates \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: tpl-spring-1" \ --json '{ "name": "spring_lamps", "language": "es", "category": "marketing", "body": "Hola {{name}}, llegó la colección de primavera a Casa Almendra. Usa el código {{coupon}} para un 10% de descuento esta semana.", "examples": { "name": "Ana", "coupon": "PRIMAVERA10" }, "phoneNumberIds": "all" }' curl https://api.lelemon.cl/v2/campaigns \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: cmp-spring-1" \ --json '{ "name": "Spring collection", "templateId": "tpl_...", "audience": { "externalRefs": ["cust-1", "cust-2"] }, "variables": { "name": "contact.name", "coupon": "PRIMAVERA10" }, "schedule": { "at": "now" }, "onReply": { "agentId": "agt_..." } }' curl -X POST https://api.lelemon.cl/v2/campaigns/cmp_.../preview \ -H "Authorization: Bearer $LELEMON_API_KEY" curl -X POST https://api.lelemon.cl/v2/campaigns/cmp_.../launch \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: launch-spring-1"