Lelemon
  • Get started
  • Use cases
  • Guides
  • API basics
  • Webhooks
  • SDK, CLI and MCP
  • API reference
  • Changelog
  • v1 (legacy)
Configure your account
    AgentsPipelinesKnowledgeContactsActions and MCP in v2Approve agent actionsUse Claude as your team's assistantConnect your back officeAPI keys
Talk to people
Run and protect
powered by Zudoku
Configure your account

Actions and MCP in v2

You will

Give an agent your own HTTP endpoints and MCP servers, and operate Lelemon from an MCP client.

You need

  • A key with actions:write
  • A public endpoint or MCP server of yours

There are two different things called MCP here. Read the one you need.

  • Your tools for an agent. You register an action (an HTTP endpoint of yours) or an MCP server and grant it to an agent. The agent calls it during a conversation.
  • The Lelemon API for your agent. An MCP client such as a coding agent or a copilot operates Lelemon through a server generated from the contract.
ContactLelemonthe agentYour backendYour MCPWrites a message1message.receivedwebhook, no text2HTTP action callLelemon-Signature, Lelemon-Case-Id3200 and the result4tools/call_meta cl.lelemon/case-id5Tool result6The reply7message.createdand case.turn_completed8
A contact writes, the agent calls your HTTP action and your MCP server with the context of the turn, and answers. Your backend learns about each step by webhook.
  1. The contact writes. A case opens or continues and the agent starts a turn.
  2. The webhook message.received tells your backend that someone wrote. It carries no text, only where to read it. Its Lelemon-Signature covers the raw body.
  3. The agent calls your action. The call is signed in Lelemon-Signature and carries Lelemon-Case-Id, Lelemon-Contact-Id, Lelemon-Tool-Id and an Idempotency-Key.
  4. Your backend verifies the signature with verifyRequest, does the work and answers with the result the agent reads.
  5. The agent calls a tool of your MCP server. The context travels in _meta (cl.lelemon/case-id, cl.lelemon/contact-id and more) and in the claims of a signed JWT.
  6. Your MCP server answers. Trust the JWT, not _meta, which is informational.
  7. The agent writes the reply to the contact on the channel where they wrote.
  8. Two more webhooks close the turn: message.created with the reply and case.turn_completed when the agent is done. Read the steps it took with GET /v2/cases/{id}/turns.

Actions

An action is one HTTP call of yours that an agent can make. The agent reads description to decide when to use it and fills input before it calls.

An action is this object. Open it to see its fields:

Actionobject, 13 fields
  • objectenumrequired

    action

  • idstringrequired

    Example act_5RvP2aKq9XwLmN3tYbZc01

  • namestringrequired

    Example create_order

  • descriptionstringrequired
  • requestActionRequestrequired
    ActionRequest4 fields
    • methodenumrequired

      GETPOSTPUTPATCHDELETE

    • originstringrequired

      Only the origin. The path and query may hold a secret.

      Example https://api.mesa.cl

    • headerNamesstring[]required

      Names only, never the values.

    • timeoutMsintegeroptional
  • inputobjectrequired
  • audienceenumrequired

    allcustomersinternal

  • noPersistbooleanrequired

    When true, what this action returns lives only in the turn that called it. It is not kept in the tool memory between turns, in the execution trace, in approval requests or in run steps. The agent still uses it to answer. Consequences: the action cannot be replayed from a stored result, and a run interrupted in the middle of a round that called it fails instead of resuming. The provider (Bedrock) still receives the content as part of the prompt of that turn.

  • sharedWithSubaccountsbooleanrequired

    Platforms only. When true, every subaccount of yours can grant this action to its own agents. The action keeps living in your organization: calls to it are signed with your signing secret and carry the Lelemon-Subaccount-Id of the subaccount that made the call. Subaccounts cannot read, change or delete it. Turning it off, or deleting the action, removes it from the agents of your subaccounts. Its name cannot match an action that one of your subaccounts already has.

  • variablesActionVariable[]required

    The fields Lelemon fills on every call. Empty when there are none.

    ActionVariable5 fields
    • namestringrequired

      Example session_id

    • sourceenumrequired

      lelemoncase_field

    • variablestringoptional

      With source lelemon. A key the catalog no longer has is ignored when the action runs.

      Example case.id

    • fieldstringoptional

      Example rut_comprador

    • optionalbooleanoptional

      With case_field.

  • authorizationActionAuthorizationRecord or nullrequired

    The authorization in force for the destination of this action: what you accepted when you created it or last changed its destination. null when there is none, for example after the destination changed and was not authorized again, and always null on an action shared by your platform.

    ActionAuthorizationRecord6 fields
    • purposestringrequired

      What the data sent to your system is used for, as it was accepted.

    • destinationCountryenumrequired

      chileoutside_chileunknown

    • versionstringrequired

      The version of the authorization text that was accepted.

      Example 2026-09-1

    • acceptedAtstring (date-time)required
    • confirmedTransferOutsideChilebooleanrequired

      True when the transfer outside Chile was confirmed apart. False when the system is in Chile.

    • dataSentstring[]required

      The names of the input fields that leave for your system, never their values.

  • createdAtstring (date-time)required
  • updatedAtstring (date-time)required
  • The URL must be public http or https. Its site must be allowed for the API key; an administrator adds sites to the key. Otherwise the answer is 403 action_origin_not_allowed.
  • Credentials go in request.headers. They are stored encrypted and never come back, only the header names do.
  • The URL is called exactly as written. A placeholder such as {id} is rejected with 400 on request.url: what changes per call goes in input (the query for GET, the body otherwise).
  • The optional authorization block (purpose, destinationCountry, version, and confirmsTransferOutsideChile outside Chile) records the authorization of article 15 bis of Law 21.719. Read the text and its version with GET /v2/actions/authorization. Send all of the block or none of it; PATCH accepts it too.
  • The action does nothing until you grant it to an agent with PUT /v2/agents/{id}/tools/{toolId}. Use audience: customers for an action that agents talking to the public may use.
  • POST /v2/actions/{id}/test calls your endpoint once with sample input. It really calls it: use sample data when the action writes. The call carries Lelemon-Signature, the context headers and an Idempotency-Key like a call of a turn, so the code that verifies the signature is tested too. It has no case, contact or actor.

MCP servers your agents call

An MCP server of yours gives an agent a set of tools. It must be a public Streamable HTTP server; OAuth servers are connected from the dashboard.

POST /v2/mcp-servers/{id}/discover answers the server, not a list: its tools come in tools. For a paginated list use GET /v2/mcp-servers/{id}/tools. The id of each tool (mtl_...) is its toolId. Grant the ones an agent may use with PUT /v2/agents/{id}/tools/{toolId}. A server whose last tool call was rejected or unanswered shows status: "error". It goes back to connected as soon as a discovery succeeds, which PATCH /v2/mcp-servers/{id} with new credentials or a new URL runs for you, or on the next tool call that works. If the server stops answering, 422 mcp_server_unreachable tells you to check the URL and the credentials; Lelemon does not follow redirects.

Connect a code repository with the GitHub MCP server

To let an agent read your code, connect the GitHub MCP server in read-only mode. No credentials go in a repository: you paste the token in the dashboard, where it is stored encrypted.

  1. In GitHub, create a fine-grained token that can only read the repositories you want to share, with read access to Contents and Metadata.
  2. In Integrations > MCP, press Conectar tu sistema. Use the name GitHub, the address https://api.githubcopilot.com/mcp/readonly, authentication Con un token, and paste the token.
  3. In Asignar a agentes, check the tools you want for each agent, for example the ones that search code and read files.

The address ends in /readonly, so GitHub only serves its read tools. The same steps work for the MCP server of a SaaS your team uses: connect it from Integrations > MCP (the catalog connects the ones with OAuth) and check its tools for the agents that need it.

System tools: hand off to a person and write case fields

Some tools come with Lelemon and you grant them like any other. Their toolId is their name.

  • transfer_to_human hands the case to a person. The case moves to human control, goes to the handoff stage and fires case.escalated.
  • save_case_data lets the agent write what the contact says into the case fields whose source is agent.
  • send_buttons, send_list, send_products, send_carousel, send_cta_url, send_flow and request_location let the agent send interactive WhatsApp messages: up to 3 reply buttons, a menu of 4 to 10 options, products from the catalog connected to the number, swipeable cards, a link button, a WhatsApp Flow or a request for a location. They take no config. What the person answers (the button, the cart, the location or the Flow fields) arrives in the next turn of the agent. They only work on WhatsApp and within 24 hours of the last message of the contact.
  • check_availability, schedule_meeting and cancel_meeting let the agent read the free times of the default agenda of the organization, book one and cancel a meeting by its id. They take no config, and meeting.scheduled and meeting.canceled fire as for any other meeting.
  • stageId is where the case goes. It wins over the pipeline's handoff stage. For a case in another pipeline it is ignored and the pipeline's own setting applies.
  • The farewell the contact reads is written by the tool, not by the model, so it cannot promise what the team will not do. businessHours picks which of the two texts is used. Without config the tool uses a neutral Spanish text.
  • config replaces the stored one on every PUT. Leave it out to clear it. DELETE removes the tool and its configuration.
  • If a reply promises to hand the case over and the case is not handed over, Lelemon asks the agent once to do it. If the second reply still does not, Lelemon hands the case over itself and the contact reads the farewell. A message that promises a handoff never goes out on a case that is still with the agent.
  • fields lists field keys. Each must be a field with source: agent that is not sensitive, in one of your pipelines; otherwise the answer is 422 unprocessable. Leave fields out to allow every non-sensitive field with source agent.
  • At run time the tool still refuses a sensitive field, a field of any other source, a key that does not exist and an empty value, whatever fields says.

Variables of Lelemon

Some fields should never come from the model. A session id the model invents is a session id that does not match. Lelemon fills these fields itself when it makes the call. The agent does not see them and cannot change them. They go in the query when the method is GET and in the body otherwise. If Lelemon does not have the value, the field is left out; it never sends an empty or made-up one.

Declare them in variables when you create the action (POST /v2/actions) or change it (PATCH /v2/actions/{id}). Each one has the name your endpoint receives and a source.

  • source: "lelemon" takes a fact Lelemon knows, named by variable (the table below).
  • source: "case_field" takes a field of the case that the pipeline declares, named by field. If the case has no value, the action does not run, unless optional is true: then the field is left out of the request.
  • PATCH replaces the whole list. Omit variables to keep the current one and send [] to remove them. The action returns them in variables.
  • A name cannot repeat or match a property of input, ignoring case. A variable that does not exist, a name that does not fit, a repeated name or a case_field without field answers 400 invalid_request with the param that failed, for example variables.0.variable. A variable that cannot go in a GET action answers action_variable_in_query, and contact.phone or contact.whatsapp_id without the authorization of the destination answers action_variable_needs_authorization.
VariableWhat your endpoint receivesPersonal data
case.idThe id of the conversation. It is a new one when the previous one was closed and the person writes againYes
case.channelThe channel it arrived byNo
agent.idThe agent that is talkingNo
organization.idYour organizationNo
contact.idThe id of the contact, ctc_.... Only in a conversation with the publicYes
contact.external_refThe externalRef you set on the contact. Only in a conversation with the publicYes
subaccount.idThe subaccount of the turn, sub_.... Missing outside a subaccountNo
actor.id, actor.role, actor.declared_byThe person you declared when you created the message, their role and the key that declared themId: yes
contact.phoneThe number the person writes from, as WhatsApp reports it. Missing if the person uses a usernameYes
contact.whatsapp_idThe WhatsApp id of the person, which Meta sends even when the person hides their numberYes

contact.phone and contact.whatsapp_id leave only from agents that talk to the public, in POST, PUT, PATCH or DELETE actions, and with a current destination authorization for that URL. Without it the action is not saved: the answer is 400 action_variable_needs_authorization, with param: authorization. The data they send shows in authorization.dataSent.

What your endpoint receives

Every call of an action carries these headers. A value that does not exist for the turn is left out, never invented.

HeaderValue
Lelemon-Case-Idcse_..., the case of the turn.
Lelemon-Contact-Idctc_..., the contact. Only in a conversation with a customer.
Lelemon-Contact-External-RefThe externalRef you set on the contact. Left out when it is not a plain header value.
Lelemon-Subaccount-Idsub_..., when the turn belongs to a subaccount.
Lelemon-Actor-Id, Lelemon-Actor-RoleThe person you declared when creating the message.
Lelemon-Actor-Declared-Bykey_..., the API key that declared the actor.
Lelemon-Tool-Idact_..., the action being called.
Idempotency-KeyOne per operation: the same turn, the same action and the same arguments (the order of the keys does not matter). If the model retries the call you get the same key: use it to avoid charging or refunding twice. A call with other arguments gets another key.
Lelemon-Signaturet=...,jti=...,v1=.... See below.

The Lelemon- prefix is reserved. A header you configure on an action can never use it, and one that tries is rejected when you save the action (and dropped on the way out for actions saved before).

A fixed Idempotency-Key configured on the action is replaced by the one of the call.

Two identical calls in the same turn are one operation; vary an argument (for example quantity) to make two.

Actions that Lelemon provides to every organization receive none of these.

Verify the signature

v1 is an HMAC-SHA256, in hexadecimal, of a text that joins: the domain lelemon-action-v1, t, jti, the method, the host, the path with its query, the SHA-256 of the raw body, and the context headers above in a fixed order, the Idempotency-Key included. Change any of them and the signature stops matching, so a signature captured from one call is useless for another route, query, body, case, contact, actor, action or organization.

Reject the call when no v1 matches, when t is more than 60 seconds from your clock, or when you have already seen the jti.

verifyRequest is not verifyWebhook: webhooks sign only the body, with another secret and another domain, and a webhook signature never verifies as an action call. Use the raw body and the full public URL, as the caller sees it.

The secret

Your organization has one secret for actions and every action gets its own, derived from it. Read the one of an action with GET /v2/actions/{id}/signing-secret and give it only to the system that serves that action.

POST /v2/actions/signing-secret/rotate creates a new one. The previous keeps signing next to the new one for 24 hours (the header carries two v1), and the list in secrets has both, so you can change your systems without a cut.

Who is speaking

For an embedded copilot, say who is speaking when you create the message:

role is end_user, staff or admin. Lelemon does not verify the actor: it passes on what your key declared and marks it with Lelemon-Actor-Declared-By. A key of a subaccount cannot declare a role above staff (403 actor_role_above_key). The actor is kept for that turn only and travels to every action and MCP server the agent calls in it, also when the call waits for an approval.

What an MCP server of yours receives

Turn on the run identity of the server so Lelemon signs its calls. Then every tools/call carries a JWT (HS256, 60 seconds) in the header you configured, and the context is in its claims.

The answer carries the secret once and its kid; the JWT header carries the same kid. Calling it again rotates: you get a new secret and the previous one keeps verifying until the next rotation, so keep both by kid. GET /v2/mcp-servers/{id}/run-identity tells you whether it is on and which kid signs, never the secret; DELETE turns it off. ?dryRun=true checks the header and the audience and says whether it would rotate, without saving anything. The header cannot be one the server already uses for its own auth.

ClaimValue
subThe actor, or the contact when there is no actor. In a run created by API, the verified user of the run.
case, contact, xref, subaccountThe same ids and reference as the action headers.
actor{ "id", "role", "by" }.
org, srv, tool, args, idem, jtiThe organization (org_…) and the server (mcp_…) as public ids, the tool, the hash of the arguments, the idempotency key and a unique id.

The same values travel in _meta of the request under the keys cl.lelemon/case-id, cl.lelemon/contact-id, cl.lelemon/contact-external-ref, cl.lelemon/subaccount-id, cl.lelemon/actor-id, cl.lelemon/actor-role and cl.lelemon/actor-declared-by. _meta is informational: trust the JWT. Without a signature there is no context, so _meta never arrives alone.

Operate Lelemon from an MCP client

A coding agent or a copilot can operate the Lelemon API through the MCP server @lelemon-ai/mcp, generated from the contract. Install, variables, filters, hidden groups and annotations are on MCP server.

Try before you do

Every route that sends a message, costs money or changes a configuration that is hard to undo accepts ?dryRun=true: sending a template, an email or a case message, reporting a contact event, testing a template, rotating a key or a secret, creating or updating campaigns, actions and triggers, replacing the recipient allowlist, erasing a contact, and launching or resuming a campaign. The call runs the same checks as the real one and answers 200 with what would happen, with dryRun: true in the body, and nothing is saved or sent. An id in that answer is not saved, and apiKeys.rotate does not show a real secret, and agents.activate answers with the agent as it would be without turning it on. It does not use the Idempotency-Key, so the real call can reuse it. A route that does not accept it answers 400 invalid_request with param: dryRun instead of doing the work.

Let the error tell you what to do

An error from a code with a known remedy carries a fix. It always comes from the error catalog and never repeats what you sent.

action is one of set or remove a parameter, retry the same request, call another endpoint first (named in endpoint as METHOD /v2/path), or stop because only a person can fix it.

ContactsApprove agent actions
On this page
  • Actions
  • MCP servers your agents call
    • Connect a code repository with the GitHub MCP server
  • System tools: hand off to a person and write case fields
  • Variables of Lelemon
  • What your endpoint receives
  • Verify the signature
  • The secret
  • Who is speaking
  • What an MCP server of yours receives
  • Operate Lelemon from an MCP client
  • Try before you do
  • Let the error tell you what to do
curl https://api.lelemon.cl/v2/actions \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: create-order-action-1" \ -H "Content-Type: application/json" \ -d '{ "name": "create_order", "description": "Create an order when the customer confirms what they want to buy.", "request": { "method": "POST", "url": "https://api.mesa.cl/lelemon/orders", "headers": { "Authorization": "Bearer your-secret" } }, "input": { "type": "object", "required": ["items"], "properties": { "items": { "type": "array", "items": { "type": "string" } } } } }'
curl https://api.lelemon.cl/v2/mcp-servers \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: crm-mcp-1" \ -H "Content-Type: application/json" \ -d '{ "name": "crm", "url": "https://mcp.mesa.cl/mcp", "auth": { "type": "bearer", "token": "your-secret" } }' curl -X POST https://api.lelemon.cl/v2/mcp-servers/$SERVER_ID/discover \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: crm-mcp-discover-1"
curl -X PUT https://api.lelemon.cl/v2/agents/$AGENT_ID/tools/transfer_to_human \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "config": { "stageId": "stg_5RvP2aKq9XwLmN3tYbZc01", "businessHours": { "timezone": "America/Santiago", "startHour": 9, "endHour": 18, "activeSaturday": false, "activeSunday": false }, "farewell": { "duringBusinessHours": "Te paso con una persona del equipo. Te escribe en breve.", "outsideBusinessHours": "Te paso con una persona del equipo. Hoy ya cerramos: te escribe mañana desde las 9." } } }'
curl -X PUT https://api.lelemon.cl/v2/agents/$AGENT_ID/tools/save_case_data \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "config": { "fields": ["comuna", "rut"] } }'
curl -X POST https://api.lelemon.cl/v2/actions \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "add_to_cart", "description": "Adds a product to the cart of the customer.", "request": { "method": "POST", "url": "https://api.mesa.cl/lelemon/cart" }, "input": { "type": "object", "properties": { "sku": { "type": "string" } } }, "variables": [ { "name": "session_id", "source": "lelemon", "variable": "case.id" }, { "name": "tax_id", "source": "case_field", "field": "rut_comprador", "optional": true } ] }'
import express from 'express'; import { createReplayGuard, verifyRequest } from '@lelemon-ai/sdk'; const app = express(); const isReplay = createReplayGuard(); app.post('/carrito', express.raw({ type: '*/*' }), async (req, res) => { try { const { context } = await verifyRequest( req.headers, req.method, `https://erp.example.com${req.originalUrl}`, req.body, LELEMON_ACTION_SECRETS, // the list from GET /v2/actions/{id}/signing-secret { isReplay, expectedToolId: 'act_...' }, ); // context.caseId, context.contactId, context.actor, context.idempotencyKey... res.sendStatus(200); } catch { res.sendStatus(401); } });
curl https://api.lelemon.cl/v2/cases/cse_.../messages \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "author": "contact", "text": "Refund my last order", "actor": { "id": "user_8841", "role": "staff", "name": "Ana Pérez" } }'
curl -X POST "https://api.lelemon.cl/v2/mcp-servers/mcp_.../run-identity" \ -H "Authorization: Bearer $LELEMON_API_KEY" -H "Content-Type: application/json" \ -d '{ "header": "x-lelemon-run", "audience": "https://mcp.example.com/mcp" }'
curl -X PUT "https://api.lelemon.cl/v2/recipient-allowlist?dryRun=true" \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "entries": ["+56912345678", "@mesa.cl"] }'
{ "code": "idempotency_key_reused", "status": 409, "fix": { "summary": "Send a new Idempotency-Key for this request. Reuse a key only to retry the exact same request.", "action": "set", "param": "Idempotency-Key", "docUrl": "https://docs.lelemon.cl/errors/idempotency_key_reused" } }