基于 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/webhooks | Webhook 的 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 不返回没有已变更文件夹的合集。
同步循环
- 第一次运行时,不带
updated_since读取整个列表,并记住本次运行开始的时刻。 - 之后每次运行时,把记住的时刻减去一个小的余量(例如 1 分钟)作为
updated_since传入,并记住新的开始时间。 - 按
id把每个对象写入您的存储,替换之前的副本。因余量而重复出现的对象只是再写一次。 - 请求后续每一页时,都带上游标和相同的
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 和事件记录组合到一个作业中。