conversations

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Conversations

对话

When to Use

使用场景

Use this skill when building anything that reads an inbox: a shared inbox UI, a helpdesk sync, a "show me this customer's history" tool, or an agent that needs the thread before it answers.
GET /v1/messages
returns a flat log with no thread to hang it on. Conversations are the thread.
当你构建任何需要读取收件箱的功能时使用此技能:共享收件箱UI、客服同步工具、“查看客户历史记录”工具,或是需要先获取对话线程再回复的Agent。
GET /v1/messages
返回的是无线程关联的扁平日志,而对话就是这些线程的集合。

What a conversation is

什么是对话

One thread with one contact, spanning every channel. A contact who writes on WhatsApp and later by email stays in the same conversation, and
channels
lists what it has carried.
FieldMeaning
id
Thread ID. Same value as
conversationId
on messages and webhooks.
contactIdentifier
The key the thread is filed under. Not always a phone number — see below.
contactId
The contact, when resolved. Absent on group threads.
channels
Every channel this thread has carried.
lastMessage
Denormalized preview (
id
,
text
,
channel
,
direction
,
at
) so a thread list needs no extra fetch.
senderId
Sender that last handled the thread. Pass it back as
Zavu-Sender
when replying.
unreadCount
Inbound messages not yet marked read.
whatsapp
bsuid
+
username
, when the contact adopted a WhatsApp username.
group
Present on group chats:
id
,
subject
,
participantCount
.
与一个联系人的单一线程,覆盖所有渠道。如果一个联系人先通过WhatsApp发送消息,之后又发邮件,这些消息会归属于同一个对话,
channels
字段会列出该对话涉及的所有渠道。
字段含义
id
线程ID。与消息和webhook中的
conversationId
值相同。
contactIdentifier
线程的归档标识。不一定是电话号码——详见下文。
contactId
已解析的联系人信息。群组线程中无此字段。
channels
该对话涉及的所有渠道。
lastMessage
非规范化的预览信息(包含
id
text
channel
direction
at
),线程列表无需额外请求即可获取该信息。
senderId
最后处理该线程的发送方。回复时需将其作为
Zavu-Sender
参数传入。
unreadCount
未标记为已读的入站消息数量。
whatsapp
当联系人设置了WhatsApp用户名时,包含
bsuid
username
group
群组聊天中存在此字段:包含
id
subject
participantCount

contactIdentifier is not a phone number

contactIdentifier 并非电话号码

It holds whichever identifier keys the thread: an E.164 phone, a WhatsApp BSUID (
US.13491208655302741918
), a numeric chat ID (Telegram/Instagram/Messenger), or a group JID (
<id>@g.us
). Parsing it as a phone number breaks on every non-SMS channel. Use
channels
and
group
to decide how to render it.
它存储的是用于标识线程的任意标识:E.164格式的电话号码、WhatsApp BSUID(如
US.13491208655302741918
)、数字聊天ID(Telegram/Instagram/Messenger),或是群组JID(如
<id>@g.us
)。若将其解析为电话号码,在非SMS渠道上会失效。请通过
channels
group
字段决定如何展示该标识。

Endpoints

接口端点

These are not in the generated SDKs yet. Use REST.
MethodPathPurpose
GET
/v1/conversations
List threads, most recently active first.
GET
/v1/conversations/{conversationId}
One thread.
GET
/v1/conversations/{conversationId}/messages
Messages in the thread, newest first.
POST
/v1/conversations/{conversationId}/read
Reset
unreadCount
to zero.
这些接口尚未包含在生成的SDK中,请使用REST方式调用。
请求方法路径用途
GET
/v1/conversations
列出线程,按最近活跃时间排序。
GET
/v1/conversations/{conversationId}
获取单个线程。
GET
/v1/conversations/{conversationId}/messages
获取线程中的消息,按最新消息排序。
POST
/v1/conversations/{conversationId}/read
unreadCount
重置为0。

List threads

列出线程

bash
curl "https://api.zavu.dev/v1/conversations?limit=25" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
Filters:
senderId
scopes to one number,
channel
keeps only threads that have carried that channel.
bash
curl "https://api.zavu.dev/v1/conversations?senderId=sender_12345&channel=whatsapp" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
TypeScript:
typescript
const res = await fetch("https://api.zavu.dev/v1/conversations?limit=25", {
  headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
});
const { items, nextCursor } = await res.json();
bash
curl "https://api.zavu.dev/v1/conversations?limit=25" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
过滤条件:
senderId
用于限定单个号码,
channel
仅保留涉及该渠道的线程。
bash
curl "https://api.zavu.dev/v1/conversations?senderId=sender_12345&channel=whatsapp" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
TypeScript示例:
typescript
const res = await fetch("https://api.zavu.dev/v1/conversations?limit=25", {
  headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
});
const { items, nextCursor } = await res.json();

Search threads

搜索线程

search
finds a thread by who it is with: phone number, email address, WhatsApp group subject, WhatsApp username, or BSUID. Phone formatting does not matter —
+1 (555) 123-4567
and
15551234567
both match the same thread.
bash
curl "https://api.zavu.dev/v1/conversations?search=%2B56912345678" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
search
参数用于按对话对象查找线程:电话号码、邮箱地址、WhatsApp群组主题、WhatsApp用户名或BSUID。电话号码格式不影响匹配——
+1 (555) 123-4567
15551234567
会匹配同一个线程。
bash
curl "https://api.zavu.dev/v1/conversations?search=%2B56912345678" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"

by email, by its local part, or by group name

按邮箱、邮箱本地部分或群组名称搜索

curl "https://api.zavu.dev/v1/conversations?search=maria"
-H "Authorization: Bearer $ZAVUDEV_API_KEY"

Matching is by whole word with prefix matching on the last term: `mar` finds `maria@example.com`, and `+1555` finds `+15551234567`. A fragment from the middle or the end of a number (`4567`) does **not** match — search the full number or a prefix.

Three things to know before you build on it:

- **It does not search message bodies.** `search` matches the thread's identity, never what was said inside it.
- **Results are ranked by relevance, not recency.** The usual "most recently active first" ordering does not apply while `search` is set.
- **An empty `search` returns nothing**, not everything. Drop the parameter to list all threads.

`search` combines with `senderId` and `channel`, and paginates with `cursor` like any other list.

```typescript
const params = new URLSearchParams({ search: "+56912345678", channel: "whatsapp" });
const res = await fetch(`https://api.zavu.dev/v1/conversations?${params}`, {
  headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
});
const { items } = await res.json();
curl "https://api.zavu.dev/v1/conversations?search=maria"
-H "Authorization: Bearer $ZAVUDEV_API_KEY"

匹配规则为整词匹配,最后一个词支持前缀匹配:`mar`会匹配`maria@example.com`,`+1555`会匹配`+15551234567`。号码中间或末尾的片段(如`4567`)**不会**匹配——请搜索完整号码或前缀。

使用前需了解三点:

- **不搜索消息内容**。`search`仅匹配线程的标识信息,不会匹配消息内的内容。
- **结果按相关性排序,而非最新活跃时间**。设置`search`参数后,默认的“按最近活跃时间排序”规则不生效。
- **空`search`参数返回空结果,而非所有线程**。如需列出所有线程,请移除该参数。

`search`可与`senderId`和`channel`组合使用,也可通过`cursor`参数分页,与其他列表接口一致。

```typescript
const params = new URLSearchParams({ search: "+56912345678", channel: "whatsapp" });
const res = await fetch(`https://api.zavu.dev/v1/conversations?${params}`, {
  headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
});
const { items } = await res.json();

Paginate

分页

nextCursor
is opaque. Pass it back verbatim as
cursor
; never build one by hand.
nextCursor
is
null
on the last page.
typescript
async function allConversations() {
  const out = [];
  let cursor: string | undefined;
  do {
    const url = new URL("https://api.zavu.dev/v1/conversations");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
    });
    const page = await res.json();
    out.push(...page.items);
    cursor = page.nextCursor ?? undefined;
  } while (cursor);
  return out;
}
The
channel
filter is applied to each page after it is fetched, so a filtered page can come back shorter than
limit
— even empty — while
nextCursor
is still set. Keep paginating until
nextCursor
is
null
; do not stop on a short page.
nextCursor
是不透明的参数。请直接将其作为
cursor
参数传回,切勿手动构造。最后一页的
nextCursor
null
typescript
async function allConversations() {
  const out = [];
  let cursor: string | undefined;
  do {
    const url = new URL("https://api.zavu.dev/v1/conversations");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
    });
    const page = await res.json();
    out.push(...page.items);
    cursor = page.nextCursor ?? undefined;
  } while (cursor);
  return out;
}
channel
过滤条件是在获取每页数据后应用的,因此过滤后的页面可能会比
limit
设定的长度短——甚至为空——但
nextCursor
仍可能存在。请持续分页直到
nextCursor
null
,不要因页面内容少而停止。

Telling the two sides apart

区分消息方向

Every message carries
direction
(
inbound
or
outbound
). Use it —
status
cannot tell them apart
, because an inbound message is stored as
delivered
too. Deriving it by comparing
to
against
contactIdentifier
works for one-to-one threads and breaks on groups, where
from
is the participant rather than the thread key.
typescript
for (const m of items) {
  const mine = m.direction === "outbound";
  console.log(`${mine ? "→" : "←"} ${m.text}`);
}
每条消息都带有
direction
字段(
inbound
表示入站,
outbound
表示出站)。请使用此字段——
status
字段无法区分消息方向
,因为入站消息的
status
也会存储为
delivered
。通过比较
to
contactIdentifier
来判断方向的方法仅适用于一对一线程,在群组线程中会失效,因为群组中的
from
是参与者而非线程标识。
typescript
for (const m of items) {
  const mine = m.direction === "outbound";
  console.log(`${mine ? "→" : "←"} ${m.text}`);
}

Read a thread

读取线程消息

bash
curl "https://api.zavu.dev/v1/conversations/$CONV_ID/messages?limit=50" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
Returns the same
Message
objects as
GET /v1/messages
, newest first, across every channel in the thread.
bash
curl "https://api.zavu.dev/v1/conversations/$CONV_ID/messages?limit=50" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
返回的
Message
对象与
GET /v1/messages
接口相同,按最新消息排序,覆盖线程涉及的所有渠道。

Mark read

标记已读

bash
curl -X POST "https://api.zavu.dev/v1/conversations/$CONV_ID/read" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
This clears the counter in your inbox. It does not send a read receipt to the contact. To show a WhatsApp read receipt plus a typing indicator, use
POST /v1/messages/{messageId}/typing
on the inbound message instead.
bash
curl -X POST "https://api.zavu.dev/v1/conversations/$CONV_ID/read" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
此操作仅清空你的收件箱中的未读计数,不会向联系人发送已读回执。如需发送WhatsApp已读回执及输入状态,请对入站消息调用
POST /v1/messages/{messageId}/typing
接口。

Replying in a thread

回复线程消息

Send with
POST /v1/messages
, addressing the thread's identifier and passing its
senderId
as
Zavu-Sender
so the reply leaves from the number the contact already knows.
typescript
const conv = await getConversation(conversationId);

await zavu.messages.send({
  to: conv.contactIdentifier,
  channel: conv.lastMessage.channel,
  text: "Thanks — shipping today.",
  "Zavu-Sender": conv.senderId,
});
The sender override is a header param inside the send params object, not a second argument.
Two constraints still apply when replying:
  • WhatsApp 24-hour window. Outside it, free-form text is rejected with
    whatsapp_window_closed
    ; send an approved template instead. See the
    whatsapp-templates
    skill.
  • Channel availability. Reply on a channel the sender actually has. Trust
    channels
    on
    GET /v1/senders/{senderId}
    , not the presence of a phone number.
调用
POST /v1/messages
发送消息,指定线程的标识,并将线程的
senderId
作为
Zavu-Sender
参数传入,确保回复使用联系人熟悉的号码发送。
typescript
const conv = await getConversation(conversationId);

await zavu.messages.send({
  to: conv.contactIdentifier,
  channel: conv.lastMessage.channel,
  text: "Thanks — shipping today.",
  "Zavu-Sender": conv.senderId,
});
发送方覆盖参数是发送参数对象中的一个头参数,而非第二个参数。
回复时仍需遵守两个限制:
  • WhatsApp 24小时窗口。超出该窗口后,自由格式文本会被拒绝并返回
    whatsapp_window_closed
    错误;请改用已审核的模板发送。详见
    whatsapp-templates
    技能。
  • 渠道可用性。请使用发送方实际拥有的渠道回复。请信任
    GET /v1/senders/{senderId}
    接口返回的
    channels
    字段,而非仅根据电话号码判断。

Getting a conversation from a webhook

从Webhook获取对话

message.inbound
carries
data.conversationId
. It is
null
while the thread row is still being created — the first inbound message of a brand-new thread, or several near-simultaneous first messages from an unknown address.
Recovery order:
  1. Use
    data.conversationId
    when present.
  2. Otherwise take it from the
    conversation.new
    event, which carries the id.
  3. Otherwise
    GET /v1/messages/{messageId}
    , whose
    conversationId
    is always populated.
Do not fall back to listing conversations and matching on the phone number: on a busy project the thread may not be at the top yet, and the identifier may be a BSUID rather than the number you expected.
message.inbound
事件包含
data.conversationId
。当线程记录仍在创建时,该字段为
null
——比如全新线程的第一条入站消息,或未知地址同时发送的多条首条消息。
恢复顺序:
  1. data.conversationId
    存在,则使用该值。
  2. 否则从
    conversation.new
    事件中获取ID。
  3. 否则调用
    GET /v1/messages/{messageId}
    接口,该接口返回的
    conversationId
    始终已填充。
请勿通过列出所有线程并匹配电话号码的方式获取:在繁忙的项目中,该线程可能尚未出现在列表顶部,且标识可能是BSUID而非你预期的电话号码。

Building an inbox

构建收件箱

The API gives you threads, messages, and read state. It does not store assignment, open/done status, internal notes, or saved replies — that workspace state belongs to your app. Key it by
conversation.id
, which is stable across channels and survives WhatsApp identity re-keying (a BSUID-only thread that later gains a phone number keeps its id).
For live updates, subscribe a webhook to
message.inbound
,
message.sent
,
message.delivered
,
message.read
,
message.failed
, and
conversation.new
, then patch your local view. Polling
GET /v1/conversations
is the fallback when you cannot receive webhooks. See the
webhook-setup
skill.
该API提供线程、消息和已读状态,但不存储分配信息、工单状态、内部备注或快捷回复——这些工作区状态属于你的应用。请使用
conversation.id
作为键,该ID在所有渠道中保持稳定,且在WhatsApp标识重新关联时(仅含BSUID的线程后来绑定了电话号码)仍会保留。
如需实时更新,请订阅Webhook的
message.inbound
message.sent
message.delivered
message.read
message.failed
conversation.new
事件,然后更新本地视图。当无法接收Webhook时,可通过轮询
GET /v1/conversations
作为备选方案。详见
webhook-setup
技能。

Gotchas

注意事项

TrapReality
Treating
contactIdentifier
as a phone
It can be a BSUID, chat ID, or group JID.
Building a cursorCursors are opaque; only echo
nextCursor
back.
Stopping pagination on a short pageThe
channel
filter shrinks pages. Stop on
nextCursor === null
.
Expecting
contactId
everywhere
Absent on group threads and unresolved contacts.
Expecting
/read
to notify the contact
It only clears your own counter.
Assuming
conversationId
is always on
message.inbound
It is
null
on a brand-new thread. Recover it as described above.
误区实际情况
contactIdentifier
视为电话号码
它可以是BSUID、聊天ID或群组JID。
手动构造cursorCursor是不透明的,仅需将
nextCursor
传回即可。
因页面内容少而停止分页
channel
过滤条件会缩短页面内容。请在
nextCursor === null
时停止分页。
认为
contactId
始终存在
群组线程和未解析的联系人中无此字段。
认为
/read
接口会通知联系人
它仅清空你自己的未读计数。
认为
message.inbound
事件中始终存在
conversationId
全新线程的该字段为
null
。请按上述恢复顺序获取。