본문으로 건너뛰기
Sursa Developers

문서 가져오기

이 가이드에서는 카드 API로 문서를 Sursa에 넣는 방법을 보여 줘요. 스크립트로 문서 몇 개를 넣을 수도 있고, 정기 작업으로 한 시스템의 모든 문서를 넣을 수도 있어요. 각 문서는 컬렉션 안의 카드가 돼요. 사람들은 검색으로 카드를 찾고, 어시스턴트는 카드를 근거로 답변하며, 카드에는 다른 카드와 똑같이 담당자, 태그, 검증, 권한이 있어요.

회사 시스템에서 어떤 문서가 삭제되었는지 Sursa가 알아서 파악하게 하려거나 연동 플랫폼에서 문서를 보낸다면 사용자 지정 소스를 대신 쓰세요. 사용자 지정 소스는 전체 동기화와 증분 동기화를 위해 만들어졌어요. 이 가이드는 컬렉션에 직접 쓰는 방법을 다뤄요.

cards:write 스코프와 대상 컬렉션에 쓸 수 있는 역할(editor 또는 admin)이 있는 API 키가 필요해요. 예시는 빠른 시작의 헬퍼를 사용해요.

알맞은 엔드포인트 고르기

가진 것사용할 엔드포인트
회사 시스템에서 고정 ID가 있는 항목PUT /v1/cards-by-external-id/{external_id}: 직접 정한 ID로 생성 또는 교체.
한 번만 추가할 텍스트(직접 정한 ID 없음)POST /v1/cards/text
한 번만 추가할 파일POST /v1/cards
여러 파일을 담은 ZIP 압축 파일POST /v1/cards/archive
요청 하나의 한도보다 큰 파일파트로 나누어 업로드한 뒤 위의 엔드포인트 중 하나.
이미 있는 카드의 새 내용PUT /v1/cards/{card_id}/content

두 번 이상 가져오는 데이터에는 external_id를 쓰세요. 그러면 모든 가져오기를 반복할 수 있어요. 다시 실행하면 사본을 더 만드는 대신 이전에 만든 카드를 업데이트해요.

직접 정한 ID로 카드 생성 또는 교체하기

PUT /v1/cards-by-external-id/{external_id}는 회사 시스템에 있는 항목의 ID를 받아요. 이 external_id를 가진 카드가 없으면 카드를 생성하고(201), 있으면 카드의 내용과 세부 정보를 교체해요(200).

curl -X PUT https://api.sursa.com/v1/cards-by-external-id/kb-article-1042 \
  -H "Authorization: Bearer $SURSA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "collection_id": 3,
    "folder_id": "0f8e2d7c-4b6a-4e1f-9c3d-2a5b8e7f1c40",
    "title": "Returning a damaged item",
    "content_type": "html",
    "body": "<h2>Damaged items</h2><p>Send a photo within 7 days and we ship a replacement.</p>",
    "tags": ["returns", "shipping"],
    "metadata": {"source": "helpdesk", "article_id": "1042"}
  }'

같은 요청을 반복하면 다음과 같이 처리돼요.

  • 내용이 바뀌지 않은 경우: 새 버전을 만들지 않아요. 응답은 200과 "status": "skipped", "reason": "unchanged"예요. Idempotency-Key가 없어도 요청을 반복하는 것은 항상 안전해요.
  • 내용이 바뀐 경우: 카드에 새 버전이 생기고 다시 인덱싱돼요. 제목, 태그, 담당자, 카테고리, metadata는 보낸 값으로 바뀌며, metadata는 통째로 교체돼요.
  • 컬렉션과 폴더는 카드를 생성할 때만 적용돼요. 나중에 카드를 옮기려면 PATCH /v1/cards/{card_id}로 변경하세요.

새 버전이 생기면 다른 내용 변경과 마찬가지로 카드의 검증이 해제돼요. 검증자가 다시 확인해야 해요.

텍스트 형식

content_type본문
markdown(기본값)Markdown.
htmlHTML. 제목, 문단, 목록, 표, http, https, mailto 링크 같은 단순한 마크업만 남기고 정리돼요. 스크립트, 스타일, 프레임, 폼, 이미지는 내용과 함께 제거돼요.
text일반 텍스트. 문단은 빈 줄로 구분해요.

카드를 기다릴지 정하기

기본적으로 요청은 카드를 찾을 수 있는 상태가 되면 "status": "ready"와 함께 card 필드에 카드를 담아 응답을 반환해요. 짧은 텍스트는 1초도 걸리지 않지만, 긴 파일은 1분 이상 걸릴 수 있어요.

대량 가져오기에는 "async": true를 보내세요. 응답이 바로 오며, "status": "queued"와 함께 job 필드에 인덱싱 작업(job_id 포함)이 담겨요. 작업은 GET /v1/jobs/{job_id}로 확인하거나 GET /v1/jobs/stream에서 모든 작업을 지켜보세요. 작업은 status가 done, error, canceled 중 하나가 되면 끝나요.

curl https://api.sursa.com/v1/jobs/48213 \
  -H "Authorization: Bearer $SURSA_API_KEY"

파일 업로드하기

파일은 multipart/form-data 요청의 file 필드로 보내요. Sursa는 PDF, Word, Excel, PowerPoint, OpenDocument 스프레드시트, CSV, Markdown, HTML, AsciiDoc, 이메일(.eml), 일반 텍스트, 일반적인 이미지 형식을 읽어요. 파일 이름, 선언된 형식, 내용이 서로 맞는지 확인하고, 맞지 않으면 415로 거부해요.

직접 정한 ID로 파일 카드를 생성하거나 교체하려면 같은 PUT /v1/cards-by-external-id/{external_id}로 파일을 보내세요.

curl -X PUT https://api.sursa.com/v1/cards-by-external-id/handbook-2026 \
  -H "Authorization: Bearer $SURSA_API_KEY" \
  -F "file=@employee-handbook.pdf" \
  -F "collection_id=3" \
  -F "tags=hr,policies" \
  -F 'metadata={"owner_team":"people"}'

폼에서 tags는 쉼표로 구분한 문자열이고, metadata는 문자열로 쓴 JSON 객체예요. title 없이 만든 새 카드는 파일 이름을 제목으로 받아요.

직접 정한 ID 없이 파일을 한 번만 추가하려면 같은 폼과 Idempotency-Key로 POST /v1/cards를 호출하세요. 컬렉션에 이미 있는 파일을 업로드하면, external_id를 보내지 않은 경우 "status": "skipped", "duplicate": true와 기존 card_id를 반환해요.

파일 하나의 크기는 설치 환경에 따라 제한돼요(기본 200MB). 더 큰 파일이나 불안정한 네트워크에서는 먼저 파일을 파트로 나누어 업로드하세요.

카드 내용 교체하기

카드의 Sursa ID를 이미 알고 있다면 PUT /v1/cards/{card_id}/content로 텍스트나 파일을 교체하고 나머지는 모두 유지할 수 있어요. 담당자, 태그, 검증자, 검증 주기, 직접 설정한 카테고리가 그대로 남아요.

curl -X PUT https://api.sursa.com/v1/cards/3f2c9a7e-5b1d-4c8e-9f0a-1b2c3d4e5f60/content \
  -H "Authorization: Bearer $SURSA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content_type": "markdown", "body": "# Refund policy\n\nRefunds are processed within 10 days."}'
  • 같은 내용을 다시 보내면 "status": "skipped"와 "reason": "unchanged"를 반환해요.
  • 지금 인덱싱 중인 카드는 409 상태와 cards.indexing_in_progress 코드를 반환해요. 몇 초 뒤에 재시도하세요.
  • 커넥터나 사용자 지정 소스 같은 소스에서 온 카드는 그 소스에서만 바꿀 수 있어요. 다른 방법으로 바꾸려 하면 cards.managed_by_source 오류가 반환돼요.
  • 키가 카드 전체를 볼 수 있어야 해요. 일부만 보이는 카드는 cards.content_whole_access 오류를 반환해요.

많은 문서 가져오기

항목마다 PUT /v1/cards-by-external-id/…를 호출하는 루프가 가장 간단한 대량 가져오기 방법이며, 언제든 멈췄다가 다시 실행해도 안전해요. 빠르게, 호출 한도에 무리가 없게 가져오려면 다음과 같이 하세요.

  1. "async": true를 보내 카드가 대기열에 들어가자마자 각 요청이 응답을 반환하게 하세요.
  2. 요청 몇 개(4~8개)를 병렬로 실행하고, RateLimit 헤더에 남은 요청이 적게 표시되면 속도를 줄이세요. 호출 한도와 할당량을 참고하세요.
  3. 429가 오면 Retry-After만큼 기다리고, 네트워크 오류나 5xx가 오면 같은 요청을 재시도하세요. 반복해도 문제없어요.
  4. 오류로 끝난 항목의 external_id를 기록해 두었다가 마지막에 다시 시도하세요.
import { api } from "./sursa.mjs";

async function importAll(records, concurrency = 6) {
  const failed = [];
  const queue = [...records];
  async function worker() {
    for (let record = queue.shift(); record; record = queue.shift()) {
      try {
        await api("PUT", `/v1/cards-by-external-id/${encodeURIComponent(record.id)}`, {
          collection_id: 3,
          title: record.title,
          content_type: "html",
          body: record.html,
          metadata: { source: "helpdesk" },
          async: true,
        });
      } catch (error) {
        failed.push({ id: record.id, error: String(error) });
      }
    }
  }
  await Promise.all(Array.from({ length: concurrency }, worker));
  return failed;
}

ZIP 압축 파일은 POST /v1/cards/archive 요청 하나로 그 안의 지원되는 파일을 각각 카드로 만들어요. 바로 계획(어떤 파일이 카드가 되고 어떤 파일이 어떤 이유로 건너뛰어지는지)과 작업을 반환하며, GET /v1/archives/{job_id}가 결과를 알려 줘요.

가져온 카드 찾기와 제거하기

자주 발생하는 오류

코드원인조치
collections.not_found이 ID나 이름의 컬렉션이 키에 보이지 않아요.GET /v1/collections로 collection_id를 확인하세요.
collections.no_write_access키가 이 컬렉션에 쓸 수 없어요.editor 역할이 있고 컬렉션에 접근할 수 있는 키를 쓰세요.
cards.external_id_taken다른 카드가 이미 가진 external_id로 POST 요청을 보냈어요.교체하려면 PUT /v1/cards-by-external-id/…를 쓰세요.
cards.managed_by_source카드가 소스에 속해 있어요.소스에서 문서를 바꾸세요.
files.type_mismatch파일 이름, 형식, 내용이 서로 맞지 않아요.실제 확장자로 파일을 보내세요.
files.too_large파일이 업로드 한도나 조직의 저장 용량을 넘었어요.파트로 나누어 업로드하거나 저장 공간을 확보하세요.
limits.cards조직이 요금제의 카드 한도에 도달했어요.필요 없는 카드를 삭제하거나 요금제를 바꾸세요.
api.metadata_invalidmetadata가 한도를 벗어났어요.메타데이터를 참고하세요.