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

Book, move and cancel meetings with an agent

You will

Let an agent offer the real free times of an agenda, book a meeting, move it and cancel it, with a reminder and meeting events.

You need

  • An API key with agents, pipelines, actions, scheduling, whatsapp and webhooks scopes
  • An https endpoint of yours that receives webhooks
  • Node.js 20 or later

A visitor asks for a call. The agent reads the free times of your agenda, offers three, books the one the person picks and confirms the day and hour. If plans change, it moves or cancels the same meeting. A reminder goes out by WhatsApp the day before, and your systems hear about every change by webhook.

Who it is for. Firms and clinics that sell an intro call or a consultation. Scopes. agents:write, pipelines:write, scheduling:read, scheduling:write, whatsapp:write, webhooks:write, cases:read, cases:write, cases:converse, contacts:write.

The agent books through three tools that Lelemon gives it: check_availability, schedule_meeting and cancel_meeting. They read and write the default agenda of the organization, so the agent never holds a scheduling key and you do not need a backend for it. You grant them like any other tool.

Code
Visitor Agent Lelemon Your endpoint | "¿cuándo?" -> check_availability -------------> free slots | <- 3 times | "el segundo" -> schedule_meeting -------------> meeting.scheduled --------> WhatsApp reminder, sendAt = start - 24 h | case moves to "Meeting booked" | "muévela" -> schedule_meeting (new) + cancel_meeting (old) | meeting.scheduled + meeting.canceled -> new reminder, old one dropped | "cancela" -> cancel_meeting -------------------> meeting.canceled ---------> reminder dropped

The script of every step is in examples/use-cases/meeting-scheduling.

0. Set up and pick the agenda

Code
import { Lelemon } from '@lelemon-ai/sdk'; const lelemon = new Lelemon({ apiKey: process.env.LELEMON_API_KEY });
Code
agenda {"id":"agd_5JKS4QUqBmhZdwhD7SdGGa","name":"Main agenda","timezone":"America/Santiago","slotDurationMinutes":30}

An empty list means the organization has not set up scheduling. Routes take the agd_ id, never the slug. See Schedule meetings.

1. Write the agent

The prompt fixes the voice, the order of questions, how times are offered (never invented, at most three, always in Santiago time), how to move a meeting and what to do when a booking fails. To move a meeting the agent books the new time first and cancels the old one after: the meeting id that schedule_meeting returned stays in its conversation, so it can cancel it later.

2. Create the pipeline and the field, then activate

When the agent books, the case moves by itself to the stage whose name says meeting (Meeting booked). That stage keeps the same agent, so the person can still ask to move or cancel the meeting. service_interest has source: "agent": the agent writes it with save_case_data.

Code
pipeline {"id":"ppl_2E381t7Oh6cOeMwLPs8Mw6","stages":["Scheduling","Meeting booked","Needs a person"]} readiness {"ready":true,"missing":[]}

3. Connect a WhatsApp number

The reminder goes out through a number of the organization, and a template can only be created when one is active. The connection returns a setupUrl; the owner of the number opens it to finish. The script reads the id of a number you already connected.

4. Create the reminder template

Reminders go out of the 24-hour window, so they are templates. A utility template with named variables is reviewed by WhatsApp: wait for approved before you use it. The script reads the id of an approved template from LELEMON_REMINDER_TEMPLATE_ID.

Code
template {"id":"tpl_5lnyg6UDq85deU8ocADCh2","status":"approved"}

5. Grant the tools

Granting is one call per tool. The scheduling tools take no configuration: they use the default agenda of the organization. save_case_data lets the agent write service_interest and transfer_to_human sends the case to Needs a person.

6. Listen to meetings

meeting.* events fire for every meeting of the organization, whoever created it: the agent, your API call or the dashboard. Each one carries the meeting. See Receive events.

7. Keep the reminder in step with the meeting

The agent books and cancels, so the reminder is yours to keep. When a meeting is scheduled, find the contact by the email the agent collected and schedule the template for 24 hours before the start with sendAt. When a meeting is canceled, cancel the reminder. A meeting moved from the dashboard or by POST /v2/scheduling/meetings/{id}/reschedule arrives as meeting.rescheduled: replace the reminder.

Code
const reminders = new Map<string, string>(); const localTime = (iso: string, timezone: string) => new Date(iso).toLocaleString('es-CL', { timeZone: timezone, weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit', }); const dropReminder = async (meetingId: string) => { const previous = reminders.get(meetingId); if (previous) await lelemon.whatsapp.messages.cancel(previous); reminders.delete(meetingId); }; const setReminder = async (meeting: { id: string; start: string; timezone: string; attendeeName: string; attendeeEmail: string }) => { const [contact] = (await lelemon.contacts.list({ query: { email: meeting.attendeeEmail } })).data; if (!contact) return; await dropReminder(meeting.id); const sendAt = new Date(Math.max(Date.parse(meeting.start) - 24 * 3_600_000, Date.now() + 60_000)); const message = await lelemon.whatsapp.messages.create({ to: { contactId: contact.id }, templateId: approvedReminder.id, variables: { name: meeting.attendeeName, when: localTime(meeting.start, meeting.timezone) }, sendAt: sendAt.toISOString(), }); reminders.set(meeting.id, message.id); }; type MeetingEvent = { type: string; data: { meeting: Parameters<typeof setReminder>[0] } }; const onMeetingEvent = async (event: MeetingEvent) => { if (event.type === 'meeting.scheduled' || event.type === 'meeting.rescheduled') await setReminder(event.data.meeting); if (event.type === 'meeting.canceled') await dropReminder(event.data.meeting.id); };

In production keep the map of reminders in your database, keyed by the meeting id.

8. Try it: book, move, cancel

Open a case for a contact and write as the contact. The agent answers in the background and you hear about it with case.turn_completed.

The agent calls check_availability and offers three times in Santiago time:

Code
contact Hola, soy Camila Soto de la Panadería Doña Rosa. Quiero ver con ustedes la contabilidad mensual. ¿Cuándo me pueden llamar esta semana? agent Hola Camila, soy Sol, de Rivas & Co. Tengo estos horarios para una llamada de 30 minutos el lunes 2 de noviembre: 1. Lunes 2 de noviembre a las 09:30 2. Lunes 2 de noviembre a las 11:30 3. Lunes 2 de noviembre a las 15:00 ¿Cuál te acomoda?

She picks one and gives her email. The agent calls schedule_meeting, confirms the day and the hour, and your endpoint receives meeting.scheduled. The case moves to Meeting booked and your handler schedules the reminder:

Code
await say('El segundo horario me sirve. Mi correo es camila@donarosa.example');
Code
contact El segundo horario me sirve. Mi correo es camila@donarosa.example agent ¡Listo, Camila! Quedó agendada tu llamada de 30 minutos para el lunes 2 de noviembre a las 11:30. Te va a llegar la invitación con el enlace de videollamada al correo camila@donarosa.example. Además, te enviaremos un recordatorio por WhatsApp el día antes. ¿Hay algo más en lo que te pueda ayudar? webhook {"type":"meeting.scheduled","meeting":{"id":"mtg_3dMEFbGkZfg3ACM722iDoY","end":"2026-11-02T15:00:00.000Z","start":"2026-11-02T14:30:00.000Z","object":"meeting","status":"scheduled","context":"Interés: contabilidad mensual. Llamada de presentación de 30 minutos.","agendaId":"agd_5JKS4QUqBmhZdwhD7SdGGa","meetLink":"https://meet.example.com/a2edda12-387","timezone":"America/Santiago","createdAt":"2026-11-01T04:46:24.802Z","updatedAt":"2026-11-01T04:46:24.802Z","canceledAt":null,"companyName":"Panadería Doña Rosa","attendeeName":"Camila Soto","cancelReason":null,"attendeeEmail":"camila@donarosa.example","attendeePhone":null,"durationMinutes":30}} case {"stage":"Meeting booked","fields":{"service_interest":"monthly_accounting"},"state":"waiting_for_contact"} reminders ["wam_29ktVd28TqXgmz3lcf3RlJ"]

To move it, she only has to ask. The agent books the new time and cancels the old meeting, so you receive meeting.canceled for the old one and meeting.scheduled for the new one, and your handler replaces the reminder. To cancel, she asks and the agent calls cancel_meeting:

Code
await say('Se me cruzó algo. ¿La puedes mover al primer horario libre de esa misma tarde?'); await say('Mejor déjala cancelada, no voy a poder esta semana.');
Code
contact Se me cruzó algo. ¿La puedes mover al primer horario libre de esa misma tarde? agent Listo, Camila. Tu llamada quedó movida para el lunes 2 de noviembre a las 12:30, y la de las 11:30 quedó cancelada. Te va a llegar la invitación con el enlace de videollamada al correo camila@donarosa.example. También te enviaremos un recordatorio por WhatsApp el día antes. webhook {"type":"meeting.canceled","meeting":{"id":"mtg_3dMEFbGkZfg3ACM722iDoY","end":"2026-11-02T15:00:00.000Z","start":"2026-11-02T14:30:00.000Z","object":"meeting","status":"canceled","context":"Interés: contabilidad mensual. Llamada de presentación de 30 minutos.","agendaId":"agd_5JKS4QUqBmhZdwhD7SdGGa","meetLink":"https://meet.example.com/a2edda12-387","timezone":"America/Santiago","createdAt":"2026-11-01T04:46:24.802Z","updatedAt":"2026-11-01T04:46:40.353Z","canceledAt":"2026-11-01T04:46:40.353Z","companyName":"Panadería Doña Rosa","attendeeName":"Camila Soto","cancelReason":"Reagendada por la persona para el primer horario libre de la tarde del mismo día","attendeeEmail":"camila@donarosa.example","attendeePhone":null,"durationMinutes":30}} webhook {"type":"meeting.scheduled","meeting":{"id":"mtg_4chqVbXPed3jfFocylQW8F","end":"2026-11-02T16:00:00.000Z","start":"2026-11-02T15:30:00.000Z","object":"meeting","status":"scheduled","context":"Interés: contabilidad mensual. Llamada de presentación de 30 minutos. Reagendada desde las 11:30 a. m. del mismo día.","agendaId":"agd_5JKS4QUqBmhZdwhD7SdGGa","meetLink":"https://meet.example.com/a0bae6b5-165","timezone":"America/Santiago","createdAt":"2026-11-01T04:46:37.812Z","updatedAt":"2026-11-01T04:46:37.812Z","canceledAt":null,"companyName":"Panadería Doña Rosa","attendeeName":"Camila Soto","cancelReason":null,"attendeeEmail":"camila@donarosa.example","attendeePhone":null,"durationMinutes":30}}
Code
contact Mejor déjala cancelada, no voy a poder esta semana. agent Listo, Camila, cancelé la llamada del lunes 2 de noviembre a las 12:30. Si más adelante quieres retomar la conversación sobre la contabilidad mensual, me escribes y te busco otro horario. webhook {"type":"meeting.canceled","meeting":{"id":"mtg_4chqVbXPed3jfFocylQW8F","end":"2026-11-02T16:00:00.000Z","start":"2026-11-02T15:30:00.000Z","object":"meeting","status":"canceled","context":"Interés: contabilidad mensual. Llamada de presentación de 30 minutos. Reagendada desde las 11:30 a. m. del mismo día.","agendaId":"agd_5JKS4QUqBmhZdwhD7SdGGa","meetLink":"https://meet.example.com/a0bae6b5-165","timezone":"America/Santiago","createdAt":"2026-11-01T04:46:37.812Z","updatedAt":"2026-11-01T04:46:47.809Z","canceledAt":"2026-11-01T04:46:47.809Z","companyName":"Panadería Doña Rosa","attendeeName":"Camila Soto","cancelReason":"La persona no podrá asistir esta semana","attendeeEmail":"camila@donarosa.example","attendeePhone":null,"durationMinutes":30}} reminders left []

The tool offers the nearest ten free times, so a request for a day beyond them is answered with those ten. A slot taken between the read and the booking makes schedule_meeting fail; the agent reads the availability again and offers another time.

9. Cancel from your own system

Your own system can cancel too, for example from an ops screen. The attendee is told, the slot is freed, the meeting stays readable with status: "canceled" and you receive meeting.canceled. A meeting that is already canceled answers 409 meeting_not_active.

Notes

  • Booking the same start twice answers 409.
  • The reminder is a message with sendAt: it is stored as scheduled and sent at that time.
  • Booking needs an active member in the organization to host the meeting; every organization has its owner.

What you see in the dashboard

  • Inbox: the case with the thread and the field service_interest in the card.
  • Pipeline: the case in Scheduling until the agent books, then Meeting booked.
  • Mi Negocio, Agendamiento: the meeting with its attendee, time and status, moved or canceled.
  • Agent, Activity: each tool call with its input and output.

What's next

  • Schedule meetings: the full reference of the scheduling API.
  • Message on events: start the conversation from your side.
  • Verify signatures of the webhooks and of the action calls.
WhatsApp sales agentSupport with approvals
On this page
  • 0. Set up and pick the agenda
  • 1. Write the agent
  • 2. Create the pipeline and the field, then activate
  • 3. Connect a WhatsApp number
  • 4. Create the reminder template
  • 5. Grant the tools
  • 6. Listen to meetings
  • 7. Keep the reminder in step with the meeting
  • 8. Try it: book, move, cancel
  • 9. Cancel from your own system
  • Notes
  • What you see in the dashboard
  • What's next
TypeScript
curl https://api.lelemon.cl/v2/scheduling/agendas \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl https://api.lelemon.cl/v2/agents \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: sol-agent-1" \ --json '{ "name": "Sol", "instructions": "$(cat instructions.md)", "llmConfig": { "model": "anthropic/claude-sonnet-5.5", "temperature": 0.2 }, "channels": ["whatsapp", "api"], "behaviors": { "displayName": "Sol" } }'
curl https://api.lelemon.cl/v2/pipelines \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: intro-pipeline-1" \ --json '{ "name": "Intro calls", "stages": [ { "name": "Scheduling", "entry": true, "agentId": "agt_..." }, { "name": "Meeting booked", "agentId": "agt_..." }, { "name": "Needs a person" } ] }' curl https://api.lelemon.cl/v2/pipelines/ppl_.../fields \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: field-service-1" \ --json '{ "key": "service_interest", "type": "select", "label": "Service", "source": "agent", "instruction": "What they want help with.", "options": [ { "value": "monthly_accounting", "label": "Monthly accounting" }, { "value": "tax_return", "label": "Tax return" }, { "value": "payroll", "label": "Payroll" }, { "value": "other", "label": "Something else" } ] }' curl -X POST https://api.lelemon.cl/v2/agents/agt_.../activate \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl https://api.lelemon.cl/v2/whatsapp/connections \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: connect-1" \ --json '{ "phone": "+56912345679", "agentId": "agt_...", "connectionType": "dedicated" }'
curl https://api.lelemon.cl/v2/whatsapp/templates \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: tpl-reminder-1" \ --json '{ "name": "meeting_reminder", "language": "es", "category": "utility", "body": "Hola {{name}}, te recordamos tu llamada con Rivas & Co. el {{when}}. Si no puedes, responde a este mensaje y la movemos.", "examples": { "name": "Camila", "when": "jueves 5 de noviembre a las 10:00" }, "phoneNumberIds": "all" }'
curl -X PUT https://api.lelemon.cl/v2/agents/agt_.../tools/check_availability \ -H "Authorization: Bearer $LELEMON_API_KEY" curl -X PUT https://api.lelemon.cl/v2/agents/agt_.../tools/schedule_meeting \ -H "Authorization: Bearer $LELEMON_API_KEY" curl -X PUT https://api.lelemon.cl/v2/agents/agt_.../tools/cancel_meeting \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl https://api.lelemon.cl/v2/webhook-endpoints \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: webhook-meetings-1" \ --json '{ "url": "https://example.com/lelemon", "enabledEvents": ["meeting.scheduled", "meeting.rescheduled", "meeting.canceled", "case.turn_completed"] }'
TypeScript
curl https://api.lelemon.cl/v2/contacts \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: contact-camila-1" \ --json '{ "name": "Camila Soto", "phone": "+56912345678", "email": "camila@donarosa.example", "externalRef": "lead-1" }' curl https://api.lelemon.cl/v2/cases \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: case-camila-1" \ --json '{ "contactId": "ctc_...", "agentId": "agt_..." }' curl https://api.lelemon.cl/v2/cases/cse_.../messages \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: msg-1" \ --json '{ "author": "contact", "text": "Hola, soy Camila Soto de la Panadería Doña Rosa. Quiero ver con ustedes la contabilidad mensual. ¿Cuándo me pueden llamar esta semana?" }'
TypeScript
TypeScript
curl "https://api.lelemon.cl/v2/scheduling/meetings?attendeeEmail=camila%40donarosa.example&status=scheduled" \ -H "Authorization: Bearer $LELEMON_API_KEY" curl https://api.lelemon.cl/v2/scheduling/meetings/mtg_.../cancel \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: cancel-1" \ --json '{ "reason": "The attendee will call back next month" }'