letsbot

n8n community nodes for LetsBot: send WhatsApp messages and templates, manage contacts and labels, and trigger workflows from LetsBot events.

Documentation

n8n-nodes-letsbot

n8n community nodes for LetsBot. Version 0.2.0 · License: MIT.

Install

In n8n: Settings → Community Nodes → Install and enter n8n-nodes-letsbot. Self-hosted with npm:
npm install n8n-nodes-letsbot in your n8n custom nodes folder (~/.n8n/nodes), then restart n8n.

Nodes

LetsBot (action node)

Resource Operation LetsBot endpoint (https://<workspace-domain>/api)
Message Send Text POST /zapier-integration/actions/send-text {phone, message, channel_id?}
Message Send Template POST /zapier-integration/actions/send-template {phone, slug, variables{}, header_media_url?, header_media_filename?, channel_id?}
Contact Create or Update POST /zapier-integration/actions/contacts {phone, name?, email?}
Contact Find by Phone GET /zapier-integration/actions/contacts/find-by-phone?phone= (404 → no items)
Contact Add Label / Remove Label POST /zapier-integration/actions/contacts/{add,remove}-label {phone, label_id | label_name}
Contact Update Custom Field POST /zapier-integration/actions/contacts/update-field {phone | contact_id, key, value}
Channel Get Many GET /zapier-integration/lookup/channels
(dropdowns) Labels, Templates, Channels, Custom Fields, Lifecycle Stages GET /zapier-integration/lookup/{labels,templates,channels,custom-fields,lifecycle-stages}

LetsBot Trigger — events: Message Received (new_message_received), Contact Created (new_contact_created),
Label Added / Removed (contact_labeled / contact_unlabeled, optional label filter), Lifecycle Stage Changed
(contact_lifecycle_changed, optional stage picked from lookup/lifecycle-stages).

  • Activate → POST /zapier-integration/webhooks/subscribe {hookUrl, trigger, name, source: "n8n", event_filters?};
    the returned id and secret are kept in the node's static data.
  • Deactivate → DELETE /zapier-integration/webhooks/unsubscribe/{id}.
  • Each delivery is checked: X-Zapier-Signature must equal sha256= + HMAC-SHA256(raw body, secret) and
    X-Zapier-Subscription-Id must match; otherwise the node answers 401 and starts nothing. The output item is LetsBot's
    flat payload.

LetsBot CRM (action node, public CRM API https://<workspace-domain>/api/v1/crm; the API key needs the
crm scope ticked under Panel → Zapier → API keys, otherwise every call answers 403 missing_scope)

Resource Operations Endpoint
Contact Create (409 → existing contact, already_existed: true), Get, Get Many (lists-engine filter JSON, text), Search (phone/email/text), Update /contacts, /contacts/{id}, /contacts/search
Company Create, Get, Get Many, Search (domain/name/text), Update, Delete, Link Contact /companies…, POST /companies/{id}/contacts
Deal Create, Get, Get Many (status/stage/owner/contact/updated since), Update, Delete, Move Stage, Mark Won, Mark Lost (lost reason) /deals…, POST /deals/{id}/{stage,won,lost}
Task Create, Get, Get Many, Update, Delete, Complete /tasks…, POST /tasks/{id}/complete
Note Create (on a contact, company or deal), Get Many (the record's timeline) POST /{contacts,companies,deals}/{id}/notes, GET /activities?subject_type=&subject_id=
(dropdowns) Stages, Lost Reasons GET /pipelines, GET /deals/lost-reasons

LetsBot CRM Trigger — any of the 18 CRM events (contact.created|updated|deleted, company.*, deal.created|updated| deleted|stage_changed|won|lost, task.created|completed, ticket.created, quote.sent|viewed|accepted).

  • Activate → POST /v1/crm/hooks {target_url, events[], source: "n8n", description}; data.id and data.secret are kept
    in static data. Deactivate → DELETE /v1/crm/hooks/{id} (404 = already gone).
  • Each delivery is checked: X-LetsBot-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.<raw body>"> with the secret, within
    5 minutes; otherwise 401. Output: the record's fields plus event, event_id, occurred_at, previous, changes
    (or the raw delivery).

Source of truth: packages/zapier/routes/tenant.php (routes), packages/zapier/src/Http/Controllers/** (bodies),
packages/zapier/src/Http/Controllers/Triggers/SamplesController.php (payloads).

Credential

LetsBot API: Workspace Domain (e.g. mystore.letsbot.net; a pasted https://…/panel is normalized) and API Key
(lbz_…, from Panel → Zapier → API keys (/panel/v2/zapier/api-keys)). Sent as X-Zapier-Api-Key. The credential test calls GET /api/user.
Scopes the key needs (a key with no scopes has full access): manage_subscriptions, read_events, send_text,
send_template, create_contact, manage_labels, update_contact, poll_contacts.

Develop

npm install          # .npmrc sets ignore-scripts: n8n-workflow pulls isolated-vm, whose native build fails on Node 26
npm run build        # tsc → dist/ + copies the SVG icons
npm run lint         # eslint (typescript-eslint + eslint-plugin-n8n-nodes-base)
npm run typecheck
npx jest test/LetsBot.node.test.ts            # one file per run (workspace rule)
npx jest test/LetsBotTrigger.node.test.ts
npx jest test/LetsBotApi.credentials.test.ts

Tests mock httpRequestWithAuthentication; nothing reaches the network.

Try it in a local n8n without publishing: npm run build && npm link, then in ~/.n8n/custom run
npm link n8n-nodes-letsbot and restart n8n. The trigger needs a public HTTPS webhook URL — LetsBot refuses
localhost/private hosts (SSRF guard). Run n8n with WEBHOOK_URL=https://<your-tunnel>/.

Known server behaviour

  • Create or Update clears an existing name/email when the field is empty (known, left as-is by owner decision).
  • Update Custom Field and the label/lifecycle triggers need the workspace's integrations area; otherwise update-field
    answers 404 not_found and those subscriptions never fire.
  • Send Text / Send Template take an optional Channel; empty = the workspace's oldest channel (as before). An id that is not one
    of the workspace's channels fails with channel_not_found and sends nothing.
  • The Custom Field and Lifecycle Stage dropdowns are empty while the workspace has not adopted the integrations area
    (stages also need the lifecycle area).
  • LetsBot has no "list subscriptions" endpoint, so checkExists trusts the id stored in static data.

Discussion