Перейти к содержанию
Sursa для разработчиков

Импорт документов

Это руководство показывает, как поместить ваши документы в Sursa через API карточек: несколько документов из скрипта или все документы системы через регулярное задание. Каждый документ становится карточкой в коллекции. Люди находят её в поиске, ассистент отвечает по ней, и у неё, как у любой карточки, есть владелец, теги, верификация и права доступа.

Если вы хотите, чтобы Sursa сама определяла, какие документы удалены в вашей системе, или отправляете документы с платформы интеграций, используйте вместо этого свой источник. Он рассчитан на полные и инкрементальные синхронизации. Это руководство описывает прямую запись в коллекцию.

Нужен ключ API с областью cards:write и роль, которой разрешено писать в нужную коллекцию (editor или admin). Примеры используют вспомогательную функцию из руководства Быстрый старт.

Какую операцию выбрать

Что у вас естьЧто использовать
Запись с постоянным идентификатором в вашей системеPUT /v1/cards-by-external-id/{external_id}: создать или заменить по вашему идентификатору.
Текст, который нужно добавить один раз, без своего идентификатораPOST /v1/cards/text
Файл, который нужно добавить один разPOST /v1/cards
ZIP-архив со множеством файловPOST /v1/cards/archive
Файл больше лимита одного запросаЗагрузка по частям, затем любая из операций выше.
Новое содержимое для уже существующей карточкиPUT /v1/cards/{card_id}/content

Для всего, что импортируется больше одного раза, используйте external_id. Тогда любой импорт можно повторить: повторный запуск обновит созданные им карточки, а не добавит копии.

Создание или замена карточки по вашему идентификатору

PUT /v1/cards-by-external-id/{external_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. Это занимает от доли секунды для короткого текста до минуты и дольше для длинного файла.

Для массового импорта передавайте "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.

Чтобы создать или заменить карточку с файлом по вашему идентификатору, отправьте файл в тот же 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 получает в качестве заголовка имя файла.

Чтобы добавить файл один раз, без своего идентификатора, используйте POST /v1/cards с той же формой и Idempotency-Key. Загрузка файла, который в коллекции уже есть, возвращает "status": "skipped" с "duplicate": true и card_id существующей карточки, если вы не передали external_id.

Размер одного файла ограничен настройками установки (по умолчанию 200 МБ). Файлы больше этого или при ненадёжной сети сначала загружайте по частям.

Замена содержимого карточки

Если идентификатор карточки в Sursa у вас уже есть, 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Ключу не видна коллекция с таким идентификатором или именем.Проверьте collection_id через GET /v1/collections.
collections.no_write_accessКлючу нельзя писать в эту коллекцию.Используйте ключ с ролью editor и доступом к коллекции.
cards.external_id_takenPOST с external_id, который уже есть у другой карточки.Используйте PUT /v1/cards-by-external-id/…, чтобы заменить её.
cards.managed_by_sourceКарточка принадлежит источнику.Измените документ в его источнике.
files.type_mismatchИмя, тип и содержимое файла не согласуются.Отправьте файл с его настоящим расширением.
files.too_largeФайл больше лимита загрузки или свободного места в хранилище организации.Используйте загрузку по частям или освободите место.
limits.cardsОрганизация достигла лимита карточек своего тарифа.Удалите ненужные карточки или смените тариф.
api.metadata_invalidmetadata нарушает ограничение.См. Метаданные.