본문으로 건너뛰기
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/webhooks웹훅의 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일 동안 보관돼요. 동기화를 그보다 오래 실행하지 않았다면 목록 전체를 다시 읽고, 목록에 더 이상 없는 모든 객체를 저장소에서 지우세요.

데이터 웨어하우스로 내보내기에서는 목록, updated_since, 이벤트 피드를 하나의 작업으로 묶어 보여 줘요.