跳到主要内容
Sursa Developers

基于 updated_since 的增量同步

要在自己的系统中保留一份 Sursa 数据的副本,不必每次都读取全部数据。带有 updated_at 的对象的列表接受 updated_since,只返回在该时刻或之后变更的对象。

curl -G https://api.sursa.com/v1/cards \
  -H "Authorization: Bearer $SURSA_API_KEY" \
  --data-urlencode "updated_since=2026-10-23T00:00:00Z" \
  --data-urlencode "limit=200"

带有 updated_since 时,列表先按 updated_at 升序、再按 id 排序,游标也按这个顺序继续。因此读取到的变更与其发生的顺序一致,分页边界也不会导致遗漏某个变更。

以下列表接受 updated_since:

列表什么算作变更
GET /v1/cards、GET /v1/cards/count卡片的 updated_at。
GET /v1/groups用户组的 updated_at:其名称或设置。
GET /v1/webhooksWebhook 的 updated_at:其设置和签名密钥,不包括投递。
GET /v1/jobs作业的 updated_at:其状态的每次变化。
GET /v1/drafts草稿的 updated_at。
GET /v1/cards/{card_id}/threads首条评论或任何回复最近一次的创建或编辑。评论串整体返回。
GET /v1/folders、GET /v1/collections/{name}/folders文件夹的 updated_at。符合条件的文件夹以扁平列表的形式返回,见下文。

合集、用户和公告没有 updated_at,因此它们的列表不按变更时间筛选:这些列表很短,每次运行时完整读取即可。时间必须带时区偏移,例如 Z 或 +02:00;其他任何值都会被拒绝,与 GET /v1/cards 相同。

带有 updated_since 时,文件夹树不再是树形结构:每个合集的 folders 以扁平列表的形式包含已变更的文件夹,其 children 为空,parent_id 表示每个文件夹所在的位置。此时 GET /v1/folders 不返回没有已变更文件夹的合集。

同步循环

  1. 第一次运行时,不带 updated_since 读取整个列表,并记住本次运行开始的时刻。
  2. 之后每次运行时,把记住的时刻减去一个小的余量(例如 1 分钟)作为 updated_since 传入,并记住新的开始时间。
  3. 按 id 把每个对象写入您的存储,替换之前的副本。因余量而重复出现的对象只是再写一次。
  4. 请求后续每一页时,都带上游标和相同的 updated_since。游标只能与第一页的筛选条件一起使用:不带这些条件时,请求返回 400,错误码为 api.cursor_invalid。

使用运行的开始时间而不是结束时间,可以确保运行期间发生的变更在下一次运行时被获取。

GET /v1/cards 返回 JSON 数组,因此下一页的信息在 X-Next-Cursor 头中(参见返回数组的列表):

const BASE = "https://api.sursa.com";
const headers = { Authorization: `Bearer ${process.env.SURSA_API_KEY}` };

async function syncCards(since, saveCard) {
  const startedAt = new Date();
  const params = new URLSearchParams({ limit: "200" });
  if (since) params.set("updated_since", new Date(since.getTime() - 60_000).toISOString());
  let query = params.toString();
  while (query) {
    const response = await fetch(`${BASE}/v1/cards?${query}`, { headers });
    if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
    for (const card of await response.json()) await saveCard(card); // write into your store by card.id
    const cursor = response.headers.get("X-Next-Cursor");
    if (cursor) params.set("cursor", cursor); // keep updated_since: the cursor needs the same filters
    query = cursor ? params.toString() : "";
  }
  return startedAt; // pass it as `since` next time
}

删除

列表只返回存在的对象,因此无法告诉您哪些对象被删除了。请从事件记录中获取删除操作:

curl -G https://api.sursa.com/v1/webhook-events \
  -H "Authorization: Bearer $SURSA_API_KEY" \
  --data-urlencode "type=card.deleted" \
  --data-urlencode "created_since=2026-10-23T00:00:00Z"

读取事件记录需要具有 admin 角色和 webhooks:manage 权限范围的密钥。事件保留 30 天。如果同步超过 30 天没有运行,请重新读取完整列表,并从您的存储中删除所有已不在列表中的对象。

导出到数据仓库把列表、updated_since 和事件记录组合到一个作业中。