Node
import { Webhook } from "standardwebhooks";
const event = new Webhook(process.env.LEGOTECH_SECRET)
.verify(rawBody, req.headers);Chatbot integrations
The bot sends what customers report to your server as signed events, gives them a case number, and passes your answer back to them in their language — on WhatsApp, Instagram, Messenger or your website.
End to end
C-48213.action.called event. Verify it, then open a ticket or hand it to a coding agent (for example Claude Code in GitHub Actions). Keep a person approving the pull request before it ships.{
"id": "6f1c…", "type": "action.called", "created_at": "2026-11-01T10:00:00+00:00",
"business_id": 5, "bot": { "id": 74, "name": "Support" },
"data": {
"ref": "C-48213", "action": "report_bug", "title": "Report a bug",
"input": { "description": "The app closes when I open the cart", "device": "iPhone 15, 2.3.1" },
"conversation_id": 1234, "channel": "whatsapp",
"reply_url": "https://legotechapps.com/lt/api/v1/cases/C-48213/reply",
"status_url": "https://legotechapps.com/lt/api/v1/cases/C-48213/status"
}
}
curl -X POST https://legotechapps.com/lt/api/v1/cases/C-48213/reply \
-H "Authorization: Bearer ltk_…" -H "Content-Type: application/json" \
-d '{"text": "Fixed in version 2.3.2 — please update the app."}'
200 = sent · 202 = waiting for the owner's approval. Send "verbatim": true to skip the rewrite into the customer's language.
order_update template (case number + your text), when it is approved and the business allows first messages; otherwise the owner is alerted with your text.Security
Headers webhook-id, webhook-timestamp, webhook-signature: v1,<base64>. Secret format whsec_…. Signature = base64(HMAC-SHA256(base64-decoded secret, "{id}.{timestamp}.{raw body}")). Reject timestamps older than 5 minutes; dedupe on webhook-id. The secret is never sent.
import { Webhook } from "standardwebhooks";
const event = new Webhook(process.env.LEGOTECH_SECRET)
.verify(rawBody, req.headers);$key = base64_decode(substr($secret, 6));
$want = 'v1,' . base64_encode(
hash_hmac('sha256', "$id.$ts.$raw", $key, true));1 min, 5 min, 30 min, 2 h, 6 h, 24 h — then the delivery is marked dead. The dashboard's delivery log shows every attempt and has “send again”.
Actions
The customer gets a case number at once; your server is called from a queue with retries. Answer later with the reply API.
Answer the POST with JSON — reply, message, answer or text is what the bot tells the customer, treated as data. Optional status, external_ref. A timeout falls back to the queue.
Opens an issue per case. Comment /reply <text> to answer, close the issue when fixed, or mention C-48213 in a release to say which version fixed it.
Reference
https://legotechapps.com/lt/apiAuthenticate with Authorization: Bearer ltk_…. Keys are created by the business owner (Chatbot → Advanced → API & cases), shown once, scoped, limited to 120 requests a minute, and optionally limited to allowed addresses (IPs or CIDR ranges — anything else gets 403 IP_NOT_ALLOWED). Every key, webhook and action change is listed in the dashboard's activity log.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /v1/me | any | Which business the key belongs to |
| GET | /v1/cases/{ref} | cases | Case details, status, can_reply_now |
| POST | /v1/cases/{ref}/reply | cases | {text, verbatim?, status?, external_ref?, external_url?} |
| POST | /v1/cases/{ref}/status | cases | {status: open|waiting|answered|closed, note?, notify?} |
| POST | /v1/hooks | hooks | Subscribe {url, event|events} → {id, secret} (Zapier REST hooks) |
| DELETE | /v1/hooks/{id} | hooks | Unsubscribe |
| GET | /v1/samples/{event} | hooks | Sample payload (a list) |
| POST | /v1/datasources/{id}/rows | data | Push rows {rows, replace?} (≤256 KB) — for a database behind a firewall |
| PUT | /v1/knowledge/{key} | knowledge | Upsert an entry {title, body, bot_id?} |
| DELETE | /v1/knowledge/{key} | knowledge | Remove it |
Events: lead.captured, handoff.requested, alert.raised, gap.noted, booking.created, order.created, action.called, case.updated, and — only when picked — message.received, message.sent. Customer name and phone are included only when the owner switched that on.
Live data
We read your JSON or CSV URL on a timer (5 minutes to daily) and search the copy. Nothing the customer types leaves us. A Google Sheet published as CSV works.
Your URL contains {query}; the bot calls it with the customer's search words (5 s timeout, falls back to the copy).
POST /v1/datasources/{id}/rows from your side. We never connect to your database.