npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@letsbot/sdk

v0.2.0

Published

Official TypeScript/Node SDK for the LetsBot developer API (/api/v1).

Readme

LetsBot TypeScript SDK (@letsbot/sdk)

Official TypeScript/Node client for the LetsBot developer API (https://letsbot.net/api/v1).

  • Zero runtime dependencies — global fetch (Node 18+), node:crypto.
  • ESM and CommonJS builds, full types generated from the OpenAPI spec.
  • Retries with exponential backoff and full jitter, honouring Retry-After.
  • An Idempotency-Key on every send, so a retried send is delivered at most once.
  • A client-side token bucket (default 1000 requests/minute, the API's per-channel limit).
  • One error type, LetsBotApiError, for every refusal — including the ones the API sends with HTTP 200/202.

Install

npm install @letsbot/sdk

License: MIT.

Quick start

import { LetsBot } from '@letsbot/sdk';

const letsbot = new LetsBot({ apiKey: process.env.LETSBOT_API_KEY! }); // the channel's lb_api_key

const sent = await letsbot.messages.send({ phone: '966501234567', body: 'Your order has shipped.' });
console.log(sent.success);

The API key is the channel's lb_api_key (panel → API → Authentication data). It is not the Zapier key (lbz_…). The workspace needs an active subscription with the API Access add-on, or an active trial.

Resources

Every endpoint in the spec is available, grouped like the API:

| Resource | Examples | |---|---| | messages | send, reply, react, forward, sendList, sendDisappearing, get, fetch, delete | | media | sendImage, sendFile, sendVideo, sendAudio, sendVoiceNote, sendLocation, sendContact, shareCatalog | | interactive | sendButtons, sendButtonsWithImage, sendButtonsTemplate…, sendCarousel | | templates | send, sendSimple, sendWithImage, sendWithDocument, sendWithVideo (official channels only) | | chats | list, conversation, read, archive, mute, pin, typing, checkPhone, … | | contacts | list, get, listLabels, addLabel, listModerators, assignModerator | | groups, invites | create, list, addParticipants, rename, getCode, sendLink, … | | user, instance, session, products | profile, presence, status, QR, catalog | | archive | list (one page), iterate (every page) |

CRM (LetsBotCrm)

The CRM API lives on the workspace's own domain (https://{workspace}/api/v1/crm) and takes a workspace API key (lbz_…, Panel → Zapier → API keys) with the crm scope ticked. The key acts as the workspace owner.

import { LetsBotCrm, verifyCrmWebhookSignature } from '@letsbot/sdk';

const crm = new LetsBotCrm({ apiKey: process.env.LETSBOT_CRM_KEY!, workspace: 'mystore.letsbot.net' });

const { data: contact } = await crm.contacts.create({ phone: '966501234567', name: 'Laila' });
const { data: deal } = await crm.deals.create({ title: 'Gift boxes', amount: 8750, contact_id: contact.id });
await crm.deals.moveStage({ id: deal.id, stage_id: 4 });
await crm.deals.addNote({ id: deal.id, body: 'Asked for a discount.' });

for await (const won of crm.iterate('crmListDeals', { status: 'won', sort: 'updated_at', dir: 'desc' })) {
  console.log(won.title, won.amount);
}

| Resource | Methods | |---|---| | contacts | list, schema, search, get, create, update, addNote | | companies | list, schema, search, get, create, update, delete, linkContact, unlinkContact, addNote | | deals | list, pipelines, lostReasons, search, get, create, update, delete, moveStage, markWon, markLost, addNote | | tasks | list, get, create, update, delete, complete | | activities, quotes, products | list, get, create (+ update, delete, schema for quotes and products) | | hooks | list, events, sample, subscribe, unsubscribe |

Every method takes one object: path parameters (id, contactId) plus the query (reads) or the JSON body (writes). Lists take q, filters (lists-engine JSON — see …schema()), view, sort, dir, page, per_page and answer {success, data, meta}. Errors are LetsBotApiError with the envelope's code (missing_scope, not_found, validation_failed, already_exists — a duplicate phone, with the existing contact in error.body.data, …). Reads are retried on network errors and 5xx; writes are not. The client-side limit defaults to 120 requests/minute, the API's per-key limit.

Webhook deliveries (panel → CRM → Settings → API & webhooks, or crm.hooks.subscribe) carry X-LetsBot-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.<raw body>">:

const valid = verifyCrmWebhookSignature(rawBody, req.headers['x-letsbot-signature'], subscriptionSecret);

Any operation can also be called by its operationId from the spec:

await letsbot.call('sendButtonsAlias', { phone: '966501234567', body: 'Confirm?', title: 'Order', footer: 'LetsBot', buttons: [{ id: 'yes', title: 'Yes' }] });
const { data, status, headers, replayed } = await letsbot.callWithResponse('getChatList');

Templates

await letsbot.templates.send({
  phone: '966501234567',
  template_name: 'order_confirmation',
  template_language: 'ar',
  components: { body: ['Ahmed', 'ORD-12345', '150 SAR'] },
});

Message archive

for await (const message of letsbot.archive.iterate({ date_from: '2026-09-01', date_to: '2026-09-07', timezone: 'Asia/Riyadh' })) {
  console.log(message.message_date, message.from_me, message.body);
}

Errors

import { LetsBotApiError, LetsBotConnectionError } from '@letsbot/sdk';

try {
  await letsbot.messages.send({ phone: '966501234567', body: 'Hi' });
} catch (error) {
  if (error instanceof LetsBotApiError) {
    error.status;    // HTTP status — can be 200 or 202, see below
    error.body;      // parsed JSON body
    error.code;      // error_code / code / error from the body, e.g. API_ADDON_REQUIRED
    error.requestId; // the support reference on a 500 (API-XXXXXXXXXX), else undefined
  } else if (error instanceof LetsBotConnectionError) {
    // no HTTP response at all (DNS, reset, timeout), after retries
  }
}

The HTTP status alone does not tell you whether a request worked. Most endpoints answer a validation failure with HTTP 202 and {"Status": 0, "Message": "..."}, and pass the connection gateway's answer through with HTTP 200 even when it says {"success": false}. The SDK treats both as failures and throws LetsBotApiError (same rule the server uses to decide whether a send counted). Pass throwOnLogicalFailure: false to get those bodies back instead.

The API has no request-id header; requestId is only filled from the reference of a 500.

Retries

  • Exponential backoff with full jitter: base 500 ms, at most 3 retries (retry: { maxRetries, baseDelayMs, maxDelayMs }).
  • Retry-After is honoured. A Retry-After longer than retry.maxRetryAfterMs (default 60 s — the workspace's daily send limit can say hours) is not waited out: the error is thrown with error.retryAfter set.
  • 429 is retried on every endpoint — it means nothing was done.
  • Network errors and 5xx are retried only when repeating is safe: on GET requests, and on send endpoints that carry an Idempotency-Key. Any other POST/DELETE (archive a chat, block a user, create a group, …) is never repeated after it may have reached the server.
  • 409 idempotency_request_in_progress is retried with the same key.
  • 409 idempotency_outcome_unknown is never retried (error.isOutcomeUnknown === true): an earlier attempt with that key died before its result was known. Check the message before sending again with a new key. Expect this after a 5xx on a send: the server marks the key "outcome unknown" on any 5xx, so the retry under the same key comes back as this 409 rather than risking a duplicate.

Idempotency

Every send endpoint (the 35 operations marked x-letsbot-send in the spec — messages.send, templates.*, media.send*, interactive.*, messages.forward, media.shareCatalog, invites.sendLink, …) gets an Idempotency-Key header: a fresh UUID per call by default, reused on every retry of that call. The server keeps the result for 24 hours and replays it for the same key (callWithResponse(...).replayed === true).

  • Pass your own key to stay idempotent across process restarts: { idempotencyKey: 'order-12345' }.
  • Pass { idempotencyKey: false } to send without one — the SDK will then not retry that send after a network error or 5xx.
  • The server honours the header on send endpoints only, so the SDK does not send it anywhere else; a key given on another endpoint is ignored and does not enable retries.

Rate limit

The client waits for a token before every attempt (default 1000 per minute, the API's per-channel limit). Use one client per API key so the bucket sees all of that key's traffic. rateLimit: { requestsPerMinute: 300 } to go slower, rateLimit: false to turn it off.

Webhooks (experimental — read this)

API webhooks (configured in the panel, API → Webhooks) are POSTed as {"event": "<type>", "data": {...}} with a Signature header: the hex HMAC-SHA256 of the raw body. The key is the literal string Letsbot, the same for every LetsBot workspace, and it is printed in this SDK. Anyone can therefore produce a valid signature:

import { experimental_verifyWebhookSignature } from '@letsbot/sdk';

experimental_verifyWebhookSignature(rawBody, req.headers['signature']); // integrity only — NOT authentication

A true does not prove the request came from LetsBot. To authenticate webhooks, add a header with a secret of your own to the webhook in the panel and compare it on receipt (see examples/webhook-receiver.ts). Verify the raw bytes you received, before JSON.parse: the server's JSON escapes / and non-ASCII characters, so a re-serialised body will not match.

In-App Chat identity

LetsBot In-App Chat (the mobile SDKs) links a conversation to your signed-in user when the app calls identify(identityToken). The identity token is a short-lived JWT (HS256) that your backend signs with the app's Identity Secret (panel → In-App Chat → your app). Never put the secret in the mobile app.

import { createIdentityToken } from '@letsbot/sdk';

// e.g. GET /api/letsbot/identity-token — behind your own authentication
app.get('/api/letsbot/identity-token', requireUser, (req, res) => {
  const token = createIdentityToken({
    userId: req.user.id,                          // sub: stable id, ≤ 191 characters
    secret: process.env.LETSBOT_IDENTITY_SECRET!,
    expiresIn: '24h',                             // default; seconds or '90s' | '15m' | '24h' | '7d', max 7 days
    name: req.user.name,                          // optional; claims win over fields the app sends
    email: req.user.email,                        // optional
    phone: req.user.phone,                        // optional, international format
  });
  res.set('Cache-Control', 'no-store').json({ token });
});

Claims: sub, iat, exp (+ optional name, email, phone). LetsBot allows 60 s of clock skew and rejects a token whose exp is more than 7 days after iat. An expired token makes identify fail with identity_expired — the app should fetch a fresh one from this endpoint and retry; a bad signature gives identity_invalid. createIdentityToken throws TypeError / RangeError on an empty secret, an empty or over-long userId, or a lifetime over 7 days. Do not log the token or the secret. Examples for PHP, Python, Go, Ruby and .NET: https://github.com/Lets-Bot/letsbot-identity-examples.

Development

npm install
npm run build                          # tsup → dist/ (ESM + CJS + .d.ts)
npx tsc --noEmit -p .                  # typecheck src, test, examples
npx vitest run test/retry.test.ts      # one spec file at a time
npm run check:drift                    # fails when anything generated is stale

The spec lives in the LetsBot workspace at packages/api/openapi/openapi.yaml; this repository keeps a copy in openapi/openapi.yaml so it builds on its own. Inside the workspace:

  • npm run spec:sync — copy the workspace spec here and regenerate src/generated/ (types via openapi-typescript, the operation table via scripts/gen-operations.mjs).
  • npm run spec:json — regenerate packages/api/openapi/openapi.json from the YAML (letsbot's OpenApiRouteParityTest reads the JSON twin).
  • npm run check:drift — vendored copy == workspace spec, generated types and operation table are fresh, and the JSON twin matches the YAML. Outside the workspace the two workspace checks are skipped. Point LETSBOT_API_SPEC at another copy of the spec to check against it.

Publishing

Published to npm as @letsbot/sdk (public, MIT). Bump version in package.json and SDK_VERSION in src/client.ts together, move the changelog entries under the new version, then npm publish --access public (prepublishOnly builds and runs the tests).