Importar documentos
Este guia mostra como colocar os seus documentos no Sursa com a API de cartões: alguns documentos a partir de um script ou todos os documentos de um sistema por meio de um job periódico. Cada documento vira um cartão em uma coleção. As pessoas o encontram na busca, o assistente responde com base nele, e ele tem responsável, etiquetas, verificação e permissões como qualquer outro cartão.
Se você quer que o Sursa descubra quais documentos foram excluídos no seu sistema, ou se você envia documentos a partir de uma plataforma de integração, use uma fonte personalizada. Ela foi feita para sincronizações completas e incrementais. Este guia trata das gravações diretas em uma coleção.
Você precisa de uma chave de API com o escopo cards:write e uma função que possa gravar na coleção de destino
(editor ou admin). Os exemplos usam a função auxiliar do
Início rápido.
Escolher o endpoint certo
| Você tem | Use |
|---|---|
| Um registro com um ID estável no seu sistema | PUT /v1/cards-by-external-id/{external_id}: criar ou substituir pelo seu ID. |
| Um texto para adicionar uma vez, sem ID próprio | POST /v1/cards/text |
| Um arquivo para adicionar uma vez | POST /v1/cards |
| Um arquivo compactado ZIP com muitos arquivos | POST /v1/cards/archive |
| Um arquivo maior que o limite de uma única requisição | Um upload em partes e depois qualquer um dos endpoints acima. |
| Um novo conteúdo para um cartão que você já tem | PUT /v1/cards/{card_id}/content |
Para tudo o que você importa mais de uma vez, use external_id. Ele torna cada importação repetível: executá-la de
novo atualiza os cartões que ela criou, em vez de acrescentar cópias.
Criar ou substituir um cartão pelo seu ID
PUT /v1/cards-by-external-id/{external_id} recebe o ID do registro no seu sistema. Se nenhum cartão tem esse
external_id, ele cria um (201). Se um cartão o tem, ele substitui o conteúdo e os detalhes do cartão (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"}
}'O que acontece em uma repetição:
- O conteúdo não mudou: nenhuma versão nova é criada. A resposta é
200com"status": "skipped"e"reason": "unchanged". Repetir a requisição é sempre seguro, mesmo sem umaIdempotency-Key. - O conteúdo mudou: o cartão ganha uma versão nova e é indexado de novo. O título, as etiquetas, o responsável, a
categoria e
metadatarecebem os valores que você enviou;metadataé substituído por inteiro. - A coleção e a pasta só valem quando o cartão é criado. Para mover um cartão depois, altere-o com
PATCH /v1/cards/{card_id}.
Uma versão nova remove a verificação do cartão, como qualquer alteração de conteúdo: um verificador o confirma de novo.
Formatos de texto
content_type | O corpo é |
|---|---|
markdown (padrão) | Markdown. |
html | HTML. Ele é limpo até restar uma marcação simples: títulos, parágrafos, listas, tabelas e links http, https e mailto. Scripts, estilos, frames, formulários e imagens são removidos com o conteúdo deles. |
text | Texto simples. Os parágrafos são separados por linhas em branco. |
Esperar pelo cartão, ou não
Por padrão, a requisição retorna quando o cartão já pode ser encontrado, com "status": "ready" e o cartão em card.
Isso leva de uma fração de segundo, para um texto curto, a um minuto ou mais, para um arquivo longo.
Nas importações em massa, envie "async": true. A resposta vem na hora, com "status": "queued" e o job de indexação
em job (com o job_id dele). Consulte o job com GET /v1/jobs/{job_id} ou acompanhe
todos os jobs em GET /v1/jobs/stream. Um job termina com status igual a done,
error ou canceled.
curl https://api.sursa.com/v1/jobs/48213 \
-H "Authorization: Bearer $SURSA_API_KEY"Enviar um arquivo
Os arquivos vão em uma requisição multipart/form-data, no campo file. O Sursa lê PDF, Word, Excel, PowerPoint,
planilhas OpenDocument, CSV, Markdown, HTML, AsciiDoc, e-mail (.eml), texto simples e os formatos de imagem comuns.
Ele confere se o nome, o tipo declarado e o conteúdo do arquivo combinam e rejeita uma divergência com 415.
Para criar ou substituir um cartão de arquivo pelo seu ID, envie o arquivo para o mesmo
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"}'Em um formulário, tags é uma string separada por vírgulas, e metadata é um objeto JSON escrito como string. Um
cartão novo sem title recebe o nome do arquivo como título.
Para adicionar um arquivo uma única vez, sem um ID próprio, use POST /v1/cards com
o mesmo formulário e uma Idempotency-Key. Enviar um arquivo que a coleção já tem retorna "status": "skipped" com
"duplicate": true e o card_id existente, a menos que você passe um external_id.
O tamanho de um arquivo é limitado pela instalação (200 MB por padrão). Para arquivos maiores, ou em redes instáveis, envie primeiro o arquivo em partes.
Substituir o conteúdo de um cartão
Quando você já tem o ID de um cartão no Sursa,
PUT /v1/cards/{card_id}/content substitui o texto ou o arquivo dele e
mantém todo o resto: o responsável, as etiquetas, os verificadores, o intervalo de verificação e uma categoria
definida à mão.
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."}'- O mesmo conteúdo de novo retorna
"status": "skipped"com"reason": "unchanged". - Um cartão que está sendo indexado neste momento retorna
409com o códigocards.indexing_in_progress. Tente de novo em alguns segundos. - Um cartão que vem de uma fonte, como um conector ou uma fonte personalizada, só é alterado na fonte dele:
cards.managed_by_source. - A chave precisa ter permissão para ver o cartão inteiro; um cartão parcialmente visível retorna
cards.content_whole_access.
Importar muitos documentos
Um loop sobre os seus registros com PUT /v1/cards-by-external-id/… é a importação em massa mais simples, e é seguro
interrompê-la e executá-la de novo a qualquer momento. Para torná-la rápida e respeitar o limite de taxa:
- Envie
"async": true, para que cada requisição retorne assim que o cartão entrar na fila. - Execute algumas requisições em paralelo (de 4 a 8) e diminua o ritmo quando o cabeçalho
RateLimitmostrar que restam poucas requisições. Veja Limites de taxa e cotas. - Em um
429, aguarde oRetry-After; em um erro de rede ou5xx, repita a mesma requisição. As repetições não causam danos. - Anote o
external_idde cada registro que terminar em erro e tente esses de novo no final.
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;
}Para um arquivo compactado ZIP, POST /v1/cards/archive transforma em cartão
cada arquivo compatível que estiver dentro dele, com uma única requisição. Ele responde na hora com o plano (quais
arquivos viram cartões, quais são ignorados e por quê) e um job;
GET /v1/archives/{job_id} informa o resultado.
Encontrar e remover os cartões importados
- Leia um cartão pelo seu ID com
GET /v1/cards-by-external-id/{external_id}. - Liste os cartões de uma importação com um filtro de metadados:
GET /v1/cards?metadata[source]=helpdesk. Veja Metadados. - Exclua um cartão com
DELETE /v1/cards/{card_id}.
Erros comuns
| Código | Causa | O que fazer |
|---|---|---|
collections.not_found | Nenhuma coleção com esse ID ou nome está visível para a chave. | Confira collection_id com GET /v1/collections. |
collections.no_write_access | A chave não pode gravar nesta coleção. | Use uma chave com a função editor e acesso à coleção. |
cards.external_id_taken | POST com um external_id que outro cartão já tem. | Use PUT /v1/cards-by-external-id/… para substituí-lo. |
cards.managed_by_source | O cartão pertence a uma fonte. | Altere o documento na fonte dele. |
files.type_mismatch | O nome, o tipo e o conteúdo do arquivo não combinam. | Envie o arquivo com a extensão real. |
files.too_large | O arquivo passa do limite de upload ou do armazenamento da organização. | Use um upload em partes ou libere espaço no armazenamento. |
limits.cards | A organização atingiu o limite de cartões do plano. | Exclua os cartões que não são mais necessários ou mude o plano. |
api.metadata_invalid | metadata ultrapassa um limite. | Veja Metadados. |