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

Support with tools and human approval

You will

Give a support agent two actions of your own and put the money-moving one behind a person, with a threshold, a cumulative cap and a hard limit.

You need

  • An API key with agents, pipelines, actions, approvals, webhooks and cases scopes
  • An https backend that serves GET /orders and POST /refunds
  • Node.js 20 or later

A customer writes that part of an order arrived damaged. The agent looks the order up, checks the item and the amount, and calls issue_refund. Small refunds run at once. When the call crosses your threshold, or the sum of the month would pass the cap, the action does not run: it becomes an approval that waits for a person. You approve it by API (or in the dashboard) and only then the action runs and the agent tells the customer.

Who it is for. Stores and services that refund or credit money and want a person to decide the big ones. Scopes. agents:write, pipelines:write, actions:write, approvals:read, approvals:write, webhooks:write, cases:write, cases:converse, contacts:write.

Code
Customer ---------> Agent --get_order----------------------> your backend (signed GET) "refund $12.000" |--issue_refund($12.000) under the rules ---> runs, agent gives the refund id "refund $18.000" |--issue_refund($18.000) month total $30.000 > cap $25.000 | \-> approval.requested (webhook) ---> person: POST /v2/approvals/{id}/approve | the action runs now '<-------- result: the agent writes to the customer

The runnable script is in examples/use-cases/support-with-approvals.

0. Set up

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

1. Register the two actions

get_order reads and issue_refund writes. Both take the customer from Lelemon, not from the model: variables with contact.external_ref makes Lelemon send the customerRef itself, so the agent cannot ask for the order of another person. Your backend checks that the order belongs to that customer.

Your server verifies the signature of each call (how) and answers JSON. sharedWithSubaccounts is only needed if you want a floor.

2. Write the agent and the pipeline

The prompt carries the policy that approvals cannot: what a refund is for, what must be confirmed, what to say for each of the three answers of issue_refund, and when to hand over. It also says that a clear request is the confirmation, so the agent does not ask twice.

3. Put refunds behind a person

PUT /v2/agents/{id}/tools/{toolId} grants the action with an approval policy. mode: "rules" takes an ordered list and the first rule that applies decides:

  • above $150.000 the call is rejected and the agent says so (reject);
  • above $20.000 a person decides (require_approval);
  • anything else runs.

accumulated adds a rolling cap per contact and action: it stops a split into small refunds, because each one is small and the sum is not. Here the month cannot pass $25.000 without a person. approvers names who may decide (here, the API key of your back office); expiresInMinutes is when the request dies and escalateAfterMinutes fires approval.escalated once.

Code
refund policy {"mode":"rules","rules":[{"when":{"param":"amount","op":"gt","value":150000},"effect":"reject","customerMessage":"A refund this large needs a manager. I am handing you to a person."},{"when":{"param":"amount","op":"gt","value":20000},"effect":"require_approval","approvers":[{"type":"key","value":"key_1LCjyzH1d3yu8glA7Yy9b1"}]}],"accumulated":{"param":"amount","period":"month","limit":25000},"approvers":[{"type":"key","value":"key_1LCjyzH1d3yu8glA7Yy9b1"}],"expiresInMinutes":240,"escalateAfterMinutes":60,"automatedDecision":false} readiness {"ready":true,"missing":[]}

get_order is granted with no policy, so it runs freely. The other tools: save_case_data for the two fields and transfer_to_human for the stage Needs a person. See Approve agent actions for every rule.

4. Set a floor that no subaccount can lower

If you run Lelemon for other companies (platform), an action you share with your subaccounts can carry a floor: the least approval any subaccount can run it with. A subaccount can be stricter, never looser, and cannot grant the action with mode: "never" below the floor. Here, any refund above $100.000 waits for an owner, whatever the policy of the subaccount says.

Code
floor {"mode":"when","condition":{"param":"amount","op":"gt","value":100000},"approvers":[{"type":"role","value":"owner"}],"expiresInMinutes":1440,"automatedDecision":false} refund tool [{"approval":"rules","floor":"when"}]

GET /v2/agents/{id}/tools shows both on the tool: approval (the policy of the agent) and approvalFloor.

5. Listen for approvals

6. Try it: a refund that runs, and one that waits

The first message names the item, the problem and the amount, so it is also the confirmation. $12.000 is under the threshold and under the cap, so issue_refund runs and the agent gives the refund id.

Code
customer Hola, mi pedido BN-2031 llegó con la cinta de corcho cortada. Quiero que me devuelvan los $12.000 de la cinta, procesa el reembolso por favor. agent Encontré tu pedido BN-2031, entregado el 28 de octubre. Ya procesé el reembolso de $12.000 por la cinta de corcho dañada, con el número RF-1. Tarda entre 3 y 5 días hábiles en llegar a tu tarjeta. refunds issued [{"id":"RF-1","orderNumber":"BN-2031","amountClp":12000,"reason":"Bar tape cork (CLP 12,000) arrived cut/damaged."}]

The second item is also under $20.000, but the month would reach $30.000 and the cap is $25.000. The call does not run:

Code
customer Y la llanta también vino con un corte. Quiero que me devuelvan los $18.000 de la llanta, procesa el reembolso por favor. agent Tu reembolso de $18.000 por la llanta quedó en revisión, así que todavía no está aprobado. Una persona del equipo lo está revisando y te responderemos en este chat dentro de las próximas cuatro horas. El reembolso de $12.000 por la cinta de corcho ya está procesado con el número RF-1. webhook {"type":"approval.requested"} approval {"id":"apr_5vQHKqxkYuaZ0UccsRSbND","status":"pending","tool":"issue_refund","arguments":{"amount":18000,"reason":"Tire 700x35 gravel (CLP 18,000) arrived with a cut.","orderNumber":"BN-2031"},"reasonCode":"policy_accumulated","summary":"issue_refund (orderNumber: \"BN-2031\", amount: 18000, reason: \"Tire 700x35 gravel (CLP 18,000) arrived with a cut.\")","approvers":[{"type":"key","value":"key_1LCjyzH1d3yu8glA7Yy9b1"}],"expiresAt":"2026-11-01T07:33:38.208Z"}

reasonCode says why: policy_accumulated here, policy_rule for the threshold, policy_always or policy_condition for the other modes. The agent told the customer that a person is reviewing it and did not call the action again.

7. A person approves

approve runs the action now with the arguments you read, then the agent resumes the conversation with the result. It has the real effect of the action, so only the approvers of the policy can call it. To say no, POST /v2/approvals/{id}/reject with a reason: the action never runs and the agent tells the customer.

Code
approval {"id":"apr_5vQHKqxkYuaZ0UccsRSbND","status":"executed","decision":{"decidedAt":"2026-11-01T03:33:41.225Z","by":{"type":"key","id":"key_1LCjyzH1d3yu8glA7Yy9b1"},"reason":"Photo of the damaged tape checked"},"result":"{\"refundId\":\"RF-2\",\"amountClp\":18000,\"status\":\"issued\"}"} agent Tu reembolso de $18.000 por la llanta 700x35 que llegó cortada ya fue emitido. El número de reembolso es **RF-2**. Debería llegar a tu tarjeta en 3 a 5 días hábiles. refunds issued [{"id":"RF-1","orderNumber":"BN-2031","amountClp":12000,"reason":"Bar tape cork (CLP 12,000) arrived cut/damaged."},{"id":"RF-2","orderNumber":"BN-2031","amountClp":18000,"reason":"Tire 700x35 gravel (CLP 18,000) arrived with a cut."}]

status: "executed" and the result of your backend. Reading the approval at GET /v2/approvals/{id} shows the whole story: who decided, when and with which reason.

Notes

  • Your backend receives real signed calls, so test the signature code too.
  • POST /v2/actions/{id}/test calls your endpoint once with sample input, without a case.

What you see in the dashboard

  • Approvals: the pending request with the action, its arguments, the reason and who may decide. Deciding there is the same as the API.
  • Inbox: the thread, with the agent's message that a person is reviewing it, and the later one with the refund id.
  • Agent, Tools: issue_refund with its policy and, for a shared action, the floor.

What's next

  • Approve agent actions: rules, approvers, roles and automated decisions.
  • Run Lelemon for your customers: share the action and fix the floor.
  • Track usage and cap conversations.
Meeting schedulingInternal copilot with a data connector
On this page
  • 0. Set up
  • 1. Register the two actions
  • 2. Write the agent and the pipeline
  • 3. Put refunds behind a person
  • 4. Set a floor that no subaccount can lower
  • 5. Listen for approvals
  • 6. Try it: a refund that runs, and one that waits
  • 7. A person approves
  • Notes
  • What you see in the dashboard
  • What's next
TypeScript
curl https://api.lelemon.cl/v2/actions \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: action-refund-1" \ --json '{ "name": "issue_refund", "description": "Refund part or all of an order to the original payment method. The amount is in Chilean pesos. Use it only after you checked the order and the customer confirmed the amount.", "audience": "customers", "sharedWithSubaccounts": true, "request": { "method": "POST", "url": "https://orders.example.com/refunds" }, "input": { "type": "object", "required": ["orderNumber", "amount", "reason"], "properties": { "orderNumber": { "type": "string" }, "amount": { "type": "number", "description": "Amount to refund in CLP, a whole number." }, "reason": { "type": "string", "description": "One sentence on why, in English." } } }, "variables": [{ "name": "customerRef", "source": "lelemon", "variable": "contact.external_ref" }] }'
curl https://api.lelemon.cl/v2/agents \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: pedro-agent-1" \ --json '{ "name": "Pedro", "instructions": "$(cat instructions.md)", "llmConfig": { "model": "anthropic/claude-sonnet-5.5", "temperature": 0.2 }, "channels": ["api"] }'
curl -X PUT https://api.lelemon.cl/v2/agents/agt_.../tools/act_... \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "approval": { "mode": "rules", "rules": [ { "when": { "param": "amount", "op": "gt", "value": 150000 }, "effect": "reject", "customerMessage": "A refund this large needs a manager. I am handing you to a person." }, { "when": { "param": "amount", "op": "gt", "value": 20000 }, "effect": "require_approval", "approvers": [{ "type": "key", "value": "key_..." }] } ], "accumulated": { "param": "amount", "period": "month", "limit": 25000 }, "approvers": [{ "type": "key", "value": "key_..." }], "expiresInMinutes": 240, "escalateAfterMinutes": 60 } }'
curl -X PUT https://api.lelemon.cl/v2/actions/act_.../approval-floor \ -H "Authorization: Bearer $LELEMON_API_KEY" \ --json '{ "mode": "when", "condition": { "param": "amount", "op": "gt", "value": 100000 }, "approvers": [{ "type": "role", "value": "owner" }] }'
curl https://api.lelemon.cl/v2/webhook-endpoints \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: webhook-approvals-1" \ --json '{ "url": "https://example.com/lelemon", "enabledEvents": ["approval.requested", "approval.decided", "approval.escalated"] }'
curl https://api.lelemon.cl/v2/contacts \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: contact-diego-1" \ --json '{ "name": "Diego Fuentes", "phone": "+56912345678", "externalRef": "cust-5001" }' curl https://api.lelemon.cl/v2/cases \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: case-diego-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-refund-1" \ --json '{ "author": "contact", "text": "Hola, mi pedido BN-2031 llegó con la cinta de corcho cortada. Quiero que me devuelvan los $12.000 de la cinta, procesa el reembolso por favor." }'
curl https://api.lelemon.cl/v2/cases/cse_.../messages \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: msg-refund-2" \ --json '{ "author": "contact", "text": "Y la llanta también vino con un corte. Quiero que me devuelvan los $18.000 de la llanta, procesa el reembolso por favor." }' curl "https://api.lelemon.cl/v2/approvals?status=pending" \ -H "Authorization: Bearer $LELEMON_API_KEY"
curl https://api.lelemon.cl/v2/approvals/apr_.../approve \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: approve-1" \ --json '{ "reason": "Photo of the damage checked" }' curl https://api.lelemon.cl/v2/approvals/apr_.../reject \ -H "Authorization: Bearer $LELEMON_API_KEY" \ -H "Idempotency-Key: reject-1" \ --json '{ "reason": "The photo shows normal wear" }'