@chatlychat/sdk-node

中文文档
0.9.0发布于 2026/11/06MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 2553

Official Node.js SDK for the Chatly API — typed REST client, webhook signature verification, and the widget identity hash.

原始使用文档
文档目录10 节
  1. Quick start
  2. What the client can do
  3. Not included, and why
  4. Idempotency
  5. Verifying anything Chatly signs
  6. The eight surfaces
  7. The unified v2 signature
  8. Rotating a secret
  9. verifyWebhookSignature (still supported)
  10. About this repository

@chatlychat/sdk-node

Server-side client for the Chatly API. Authenticates with an API key, so it is for your backend — not a browser, where the key would be readable.

pnpm add @chatlychat/sdk-node

Quick start

import { ChatlyClient } from '@chatlychat/sdk-node';

const chatly = new ChatlyClient({
  apiKey: process.env.CHATLY_API_KEY!, // ck_live_…
  apiUrl: 'https://api.chatlychat.com', // optional
});

const contacts = await chatly.contacts.list({ limit: 20 });

Create a key under Workspace → Developer → API keys. The token is shown once and stored only as a hash — if you lose it, revoke and mint another.

Make whoami() your first call. It returns the workspace the key belongs to and the scopes it actually holds, so a mis-scoped key fails loudly at integration time instead of surfacing as a puzzling 403 from an unrelated endpoint in production:

const { workspaceId, scopes } = await chatly.whoami();

Seven of the api-keys routes accept a key, minting among them since 2026-10-26: create refuses any scope the calling key does not hold itself, so a leaked read-only key cannot escalate. Only PUT /v1/api-keys/:id/machine-user stays closed to keys. The ones with no typed method here are reachable through request(), which takes headers — that is how you send x-secret-recipient to have a new key's secret sealed to a public key instead of returned in plaintext.

What the client can do

Every method below maps to an endpoint that accepts key authentication. Scope the key to only what your integration needs; a key can never do more than the scopes ticked when it was created.

CallScope required
chatly.whoami()workspace:read
chatly.contacts.list({ cursor?, limit? })contacts:read
chatly.contacts.upsert({ email, name, … })contacts:write
chatly.conversations.list({ cursor?, limit? })conversations:read
chatly.conversations.messages(conversationId)conversations:read
chatly.conversations.send({ conversationId, body })messages:write
chatly.search.query('refund', { … })search:read
chatly.search.kb('refund')search:read
chatly.events.track({ contactId, name, properties })contacts:write

Messages you send with a key are recorded as sent by an integration, not by the person who created the key — a key outlives whoever issued it, so attributing its writes to them would be wrong the moment they leave.

Not included, and why

Reactions and channel/widget configuration are absent on purpose. A reaction records who reacted and a key is not a person, so those endpoints require a signed-in user. Channel configuration needs a scope no key can hold. Both used to be here and both returned 403 to every caller.

Idempotency

Writes accept an idempotency key so a retry after a network failure does not duplicate work:

await chatly.withIdempotencyKey('order-4471-refund').contacts.upsert({ … });

Verifying anything Chatly signs

verifyChatlySignature is the one to use. You tell it which surface you are terminating, hand it the raw body and the request's headers, and it checks the construction that surface actually uses:

import { verifyChatlySignature } from '@chatlychat/sdk-node';

const result = verifyChatlySignature({
  surface: 'event_webhook',
  body: rawBody, // the EXACT bytes, before JSON.parse
  headers: req.headers,
  secret: process.env.CHATLY_WEBHOOK_SECRET!,
  method: req.method, // see "the unified v2 signature" below
  path: req.url,
});

if (!result.ok) {
  console.warn('rejected', result.reason, result.message);
  return res.status(400).end();
}

Pass the raw body — re-serialising parsed JSON reorders keys and changes whitespace, so the digest will not match. On a refusal, reason is a stable string to branch on (signature_mismatch, timestamp_outside_tolerance, missing_signature_header, …) and message is a sentence written to be printed. It never throws.

The eight surfaces

surfaceHeader(s)Signed stringt unit
unified_v2x-chatly-signature-v2: t=…,v1=…<t>.<METHOD>.<path>.<rawBody>seconds
agent_deliveryx-chatly-signature: t=…,v1=…<t>.<rawBody>seconds
event_webhookx-livechat-signature: t=…,v1=…<t>.<rawBody>seconds
ai_tool_callx-chatly-signature: t=…,v1=…<t>.<METHOD>.<path>.<rawBody>seconds
canvas_stepx-chatly-canvas-signature: sha256=… and x-chatly-canvas-ts<ts>.<rawBody>milliseconds
customer_cardx-chatly-signature: sha256=… and x-chatly-timestamp<ts>.<rawBody>milliseconds
workflow_webhookx-chatly-signature-v2: t=…,v1=…<t>.<rawBody>seconds
workflow_webhook_legacyx-chatly-signature: sha256=…<rawBody> — no timestampn/a

Three of them send x-chatly-signature and sign three different strings, which is why surface is required rather than inferred from the header: a verifier that keys on the header name can be pointed at the wrong construction while looking completely correct.

ai_tool_call additionally needs method and path (pathname + search, never the host), because both are inside the signed string. toleranceSeconds defaults to 300 and is always in seconds, whatever unit the surface's timestamp uses — the conversion is this function's job, and getting it wrong by hand puts the freshness check about 54,000 years out and rejects everything.

Prefer workflow_webhook over workflow_webhook_legacy. Every workflow webhook call sends both headers. The legacy one signs the body with no timestamp, so a captured request verifies forever and nothing can bound a replay; verifyChatlySignature refuses it unless you pass allowUnprotectedReplay: true, and if you do, make the endpoint idempotent.

The unified v2 signature

Every surface is moving to one construction:

x-chatly-signature-v2: t=<unix seconds>,v1=<hex>
v1 = HMAC-SHA256(secret, `${t}.${METHOD}.${path}.${rawBody}`)

It is sent alongside each surface's existing header, so nothing breaks while senders migrate. path is pathname + search of the request as it arrived — never the scheme, never the host. The method and the path are inside the digest because a GET with no body otherwise signs "<t>..", which is identical for every GET that surface ever makes and replayable against any path.

Pass method and path on every call and you need no code change when a sender moves: v2 defaults to 'prefer', so the v2 header is verified the moment it appears and the legacy construction is verified until then.

const result = verifyChatlySignature({
  surface: 'customer_card',
  body: rawBody,
  headers: req.headers,
  secret: process.env.CHATLY_CARD_SECRET!,
  method: req.method,
  path: req.url,
  v2: 'prefer', // the default: 'require' once you know the sender has moved
});
// result.surface is 'unified_v2' when the v2 header was the one that verified.

'prefer' never falls back: if the v2 header is present and does not verify, that is a refusal rather than a reason to try the legacy header. Both headers come from the same secret, so a good legacy digest beside a bad v2 one is tampering or a sender bug, and accepting it would hide both. Use v2: 'ignore' to pin the old behaviour.

One wrinkle, and it is ours: workflow_webhook already sends x-chatly-signature-v2, over <t>.<rawBody> — that header predates the unified base. So on that surface both bases are accepted whenever you supply method and path, and result.base ('body' or 'target') tells you which one the sender used.

The value carries a base marker: t=…,v1=…,b=t for the unified base, b=b for <t>.<rawBody>. An absent marker means <t>.<rawBody>, because every v2 value sent before the marker existed was that. The verifier reads it to narrow what it tries and to name the cause when it refuses — a b=t workflow request with no method/path is missing_request_target, not a mismatch — but never to accept more than the surface allows, since anyone on the wire can write a marker.

Which surfaces send x-chatly-signature-v2 today: agent delivery, the event webhook, customer-card fetches and canvas / step callbacks, all b=t; the workflow action, b=b. AI tool calls do not send it yet — their legacy x-chatly-signature already signs the unified string, so 'prefer' falls through to an identical check and you lose nothing by passing method and path now.

If you terminate several surfaces, verify them all as unified_v2. That is the one documented path for the v2 header; the per-surface names are for the legacy headers. The single exception, until its sender moves to b=t, is the workflow action — verify that as workflow_webhook.

Rotating a secret

Both ends of a rotation are supported:

const result = verifyChatlySignature({
  surface: 'agent_delivery',
  body: rawBody,
  headers: req.headers,
  secret: [process.env.CHATLY_SECRET_NEW!, process.env.CHATLY_SECRET_OLD!],
});
if (result.ok) {
  console.log('verified with secret', result.secretIndex); // 0 = the new one
}
  • secret takes an array. Accept the old and the new secret for as long as the overlap lasts; result.secretIndex is how you learn the old one has stopped being used.
  • The header can carry a list of digests — t=…,v1=…,v1=… — for a sender signing with two secrets at once. Any one matching verifies; every one of them has to be well formed.

verifyWebhookSignature (still supported)

The original single-scheme verifier. It handles event_webhook only and takes the header value rather than the header bag. It is published, in use, and not going away:

import { verifyWebhookSignature } from '@chatlychat/sdk-node';

const ok = verifyWebhookSignature({
  body: rawBody, // the EXACT bytes, before JSON.parse
  signatureHeader: req.headers['x-livechat-signature'] as string,
  secret: process.env.CHATLY_WEBHOOK_SECRET!,
});

The scheme is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over `${t}.${body}`. The timestamp is inside the signed string, so a captured delivery stops verifying once it falls outside toleranceSeconds.

This example was wrong until 2026-10-12, in three ways — it said payload (the field is body), signature (it is signatureHeader) and x-chatly-signature (webhooks use x-livechat-signature). In TypeScript the old snippet was a compile error; in plain JavaScript it was worse than returning false, because the verifier splits input.signatureHeader first and an absent one throws a TypeError out of your webhook handler.

The header name is the trap worth knowing: x-chatly-signature is real, but it belongs to four other surfaces — external-agent deliveries, custom AI tool calls, customer cards and the workflow fire_webhook action — and each of those constructs its base string differently. Outbound event webhooks are the one surface using x-livechat-signature. Do not share one verifier across them without checking the construction.


About this repository

This is a mirror. @chatlychat/sdk-node is built from a private monorepo where the rest of the platform, its migrations and its contract checks live, so a pull request here cannot be merged directly — but a patch is still the fastest way to get something fixed and we will port it with attribution.

What you can do here: read the source, run the tests, open an issue, or open a PR and we will treat the diff as the proposal.

Not mirrored: src/signature-vectors-public.spec.ts — specs that assert a type restated here still matches the monorepo's original, which is only a meaningful claim inside it. They keep running there.

Issues: https://github.com/chatlychat/chatly-sdk-node/issues · Docs: https://chatlychat.com/docs

版本与发布记录

@chatlychat/sdk-node 共发布 16 个版本,首次发布于 2026/10/13;过去 12 个月发布了 16 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.9.0latest2026/11/063 天前
0.8.02026/10/2713 天前
0.7.42026/10/2614 天前
0.7.32026/10/2614 天前
0.7.12026/10/1723 天前
0.7.02026/10/1723 天前

关于 @chatlychat/sdk-node 的常见问题

@chatlychat/sdk-node 是什么?

Official Node.js SDK for the Chatly API — typed REST client, webhook signature verification, and the widget identity hash.

如何安装 @chatlychat/sdk-node?

在项目目录运行 npm install @chatlychat/sdk-node,或使用 pnpm add / yarn add / bun add。

@chatlychat/sdk-node 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@chatlychat/sdk-node 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@chatlychat/sdk-node 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@chatlychat/sdk-node 可以商用吗?

@chatlychat/sdk-node 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@chatlychat/sdk-node 的下载量有多少?

根据 npm 官方下载统计,@chatlychat/sdk-node 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,553 次,日均约 85 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@chatlychat/sdk-node 还在维护吗?

最新版本 0.9.0 发布于 2026/11/06(3 天前);至今共发布 16 个版本,其中过去 12 个月发布了 16 个;首次发布于 2026/10/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。