Skip to content
Sursa Developers

Sync your system with a custom source

A custom source connects a system that has no built-in connector: your wiki, your ticketing tool, a database, an export from anywhere. Your code pushes documents to Sursa, and Sursa turns them into cards in the collection and folders that the source is set up for, with the same search, answers, verification and permissions as cards from any other source.

Unlike writing cards directly, a custom source keeps track of what you have sent. At the end of a full sync, Sursa archives every document that you did not send, so deletions in your system reach Sursa without extra work.

Set up the source

An administrator creates the source in Sursa under Sources → New source → Custom source, and chooses:

  • the collection and, optionally, a folder for its cards;
  • the folder mode: all cards in the chosen folder, or sub-folders that mirror the path of each document;
  • the permissions: the permissions of the collection, or "as in the source", where each document lists who may see it.

The source page shows its ID; the examples use 42.

Choose a key

KeyStarts withCan do
Source keykbsrc_Write documents into this one source. Nothing else: it cannot read or search. Create it on the source page under API keys.
Organization key with the import:write scopekb_Write into any custom source of the organization and read its state. Creating a source and its keys needs the admin scope.

The source key is the safest choice for a single integration, especially one that runs on a third-party platform. An organization key with import:write suits a service that feeds several sources.

Both keys are sent the same way, and every push endpoint lives under /v1/push/sources/{source_id}. Check the key and the state of the source with GET /v1/push/sources/{source_id}:

curl https://api.example.com/v1/push/sources/42 \
  -H "Authorization: Bearer $SURSA_SOURCE_KEY"
{"source_id": 42, "name": "Helpdesk articles", "documents": 1840, "open_sync": null}

The document

FieldRequiredDescription
external_idyesYour stable ID of the document, up to 512 characters. Sending the same ID again replaces the document.
titleyesCard title, up to 500 characters. For a file, the file name by default.
contentyes, unless there is a fileUp to 5 MB.
content_typenohtml (default), markdown or text. HTML is cleaned of scripts, styles, forms and embedded objects.
urlnoLink to the document in your system, shown on the card.
pathnoFolders, as an array (["Support", "Billing"]) or a string ("Support/Billing"), up to 10 levels.
permissionsno{"everyone": true}, or {"emails": [...], "groups": [...]}. Used when the source permissions are "as in the source".
tagsnoUp to 50 tags, added to the tags of the source.
author_emailnoIf this is a user of the organization, they become the owner of the card.
updated_atnoWhen the document changed in your system (RFC 3339).
metadatanoYour key-value pairs on the card. See Metadata.
categorynoCategory of the card: the key of a category of the organization.
verifiers, verify_interval_daysnoWho verifies the card (users by e-mail and groups) and how often, in days.
upload_idnoThe file of the document from an upload in parts, instead of content.
attachmentsnoUp to 20 more files of the document, as a list of upload_id. Each becomes its own card of the source.

Instead of content, a single document can carry a file of up to 100 MB in a multipart/form-data request: the file in the file part, the fields above as JSON in the document part. Uploads in parts for upload_id and attachments are created with an organization key that has the import:write scope and the purpose import_attachment; a source key cannot create them.

Two ways to push

Direct updates suit systems that tell you about each change, such as a webhook from your helpdesk or a step in Zapier. Send a document when it is created or changed, and delete it when it is removed:

RequestDoes
POST /v1/push/sources/{source_id}/documentsCreates or replaces one document.
POST /v1/push/sources/{source_id}/documents/batchCreates or replaces many documents at once.
DELETE /v1/push/sources/{source_id}/documents/{external_id}Removes one document. URL-encode the ID.

Changes become cards within about a minute; several changes in a row are processed together.

Syncs suit regular exports: you send everything, or everything that changed, and Sursa reconciles. The rest of this guide is about syncs.

Run a full sync

A sync has three steps: open, push, close.

1. Open

POST /v1/push/sources/{source_id}/syncs opens a sync and returns its number:

curl -X POST https://api.example.com/v1/push/sources/42/syncs \
  -H "Authorization: Bearer $SURSA_SOURCE_KEY" \
  -H "Idempotency-Key: sync-2026-10-24" \
  -H "Content-Type: application/json" \
  -d '{"type": "initial", "max_minutes": 180}'
{"sync_number": 7, "type": "initial", "status": "open", "opened_at": "2026-10-24T02:00:00Z",
 "expires_at": "2026-10-24T05:00:00Z", "pushed": 0, "removed": 0}
  • type is initial for the first sync of the source, and incremental once a sync has completed. An incremental sync before any completed one returns sources.push_initial_required.
  • Only one sync can be open at a time: a second one returns 409 sources.push_sync_open.
  • A sync stays open for max_minutes (60 by default, up to 1440). After that it expires: nothing is archived and its number is no longer accepted. Choose a time comfortably longer than your export takes.

2. Push documents

Send the documents in batches of up to 500, with the sync number. A batch body is up to 50 MiB; files inside a batch are passed as upload_id.

curl -X POST https://api.example.com/v1/push/sources/42/documents/batch \
  -H "Authorization: Bearer $SURSA_SOURCE_KEY" \
  -H "Idempotency-Key: sync-7-batch-1" \
  -H "Content-Type: application/json" \
  -d '{
    "sync_number": 7,
    "documents": [
      {"external_id": "faq-shipping", "title": "Shipping", "content_type": "markdown",
       "content": "# Shipping\n\nWe ship worldwide within 5 days.", "path": ["Support"]},
      {"external_id": "faq-refunds", "title": "Refunds", "content_type": "html",
       "content": "<p>Refunds take 14 days.</p>", "path": ["Support", "Billing"],
       "permissions": {"groups": ["Support"]}, "metadata": {"article_id": "1042"}}
    ]
  }'

The response has a result for each document, in the order you sent them:

{
  "results": [
    {"external_id": "faq-shipping", "result": "unchanged", "status": "indexed", "card_id": "8c1d…", "version": "5e0f…"},
    {"external_id": "faq-refunds", "result": "updated", "status": "pending", "card_id": "3f2c…", "version": "a91b…"}
  ]
}
FieldMeaning
resultWhat happened to the document now: created, updated, unchanged, deleted, not_found, error, or skipped (in an atomic batch that was not applied).
statusWhere the document is on its way to a card: pending, processing (the card is written and is being indexed), indexed, failed or removed.
card_idThe card of the document, once it exists.
versionA hash of the content and the fields. The same document sent again has the same version and is unchanged.
error, reasonWhy the document failed. reason is a stable code: content_invalid, file_rejected, too_large, limit_reached, index_failed, or batch_rejected (another document of an atomic batch failed).

By default, every document of a batch is applied on its own: one bad document does not stop the others.

Documents that did not change since the last sync do not have to be sent again. Confirm them by ID with POST /v1/push/sources/{source_id}/syncs/{sync_number}/touch, up to 1000 IDs per request, so that the full reconciliation keeps them:

curl -X POST https://api.example.com/v1/push/sources/42/syncs/7/touch \
  -H "Authorization: Bearer $SURSA_SOURCE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_ids": ["faq-contact", "faq-hours"]}'

3. Close

POST /v1/push/sources/{source_id}/syncs/{sync_number}/close ends the sync:

curl -X POST https://api.example.com/v1/push/sources/42/syncs/7/close \
  -H "Authorization: Bearer $SURSA_SOURCE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "complete"}'
statusEffect
completeFull reconciliation. Every document of the source that was neither pushed nor touched in this sync is archived. Use it only when you sent everything.
complete_incrementalApplies what you sent and archives nothing.
failedArchives nothing and keeps what was already pushed. Add reason (for example API_ERROR) and error_details for the log.

Close with failed whenever your export stops halfway: a complete after a partial export would archive every document you did not get to. Repeating the same close returns the same result.

After the close, Sursa turns the documents into cards within about a minute. The source page shows each sync with the number of documents pushed and archived, and GET /v1/push/sources/{source_id}/syncs/{sync_number} returns the same counters.

Incremental syncs

Between full syncs, send only what changed:

  1. Open with "type": "incremental".
  2. Push the changed documents, and put the IDs of removed ones into deletes (below).
  3. Close with complete_incremental.

Run a full sync from time to time, for example weekly, to catch anything an incremental run missed.

Delete documents in a batch

A batch can remove documents together with the ones it writes. List their IDs in deletes:

{
  "sync_number": 8,
  "documents": [{"external_id": "faq-shipping", "title": "Shipping", "content": "…"}],
  "deletes": ["faq-old-returns", "faq-fax"]
}

Each deleted ID gets its own entry in results, with result deleted, or not_found if the source has no such document. Deleting the same ID again is harmless.

Atomic batches

Some changes only make sense together, for example a document and the one that replaces it. Send "atomic": true, and the batch is applied completely or not at all: if any document fails its checks, nothing of the batch is applied, and the response is 422 with the code sources.push_batch_rejected and the result of every document in error.details.results: the documents that failed have error, the others skipped.

{"sync_number": 8, "atomic": true, "documents": […], "deletes": ["faq-old-returns"]}

Without atomic, a batch is applied document by document. Use atomic batches for related changes, and regular batches for bulk loads, where one bad document should not hold back the rest.

Check the result of each document

The batch response tells you how each document was written. Turning it into a card happens afterwards, so status can still be pending. To find the documents of a sync that did not become cards:

curl "https://api.example.com/v1/push/sources/42/syncs/7/documents?status=failed" \
  -H "Authorization: Bearer $SURSA_SOURCE_KEY"

Each entry has external_id, title, status, card_id, version and reason, and the list pages with cursor. status also takes pending, processing, indexed and removed.

You can also be told instead of asking: the events import.sync_completed (a sync is closed and processed, with its totals) and import.document_failed (a document did not become a card, with its reason) arrive by webhook and in the event feed.

Retries

  • Send an Idempotency-Key with every POST. A repeated batch with the same key returns the first result.
  • Even without the key, a document with the same external_id never becomes a second card: it is unchanged or updated.
  • If the process stops in the middle of a sync, open a new one after the old one expires, or close the old one with failed if you still have its number, and run the sync again.

Limits

LimitValue
Content of one document5 MB
File in a single-document request100 MB
Documents per batch500
Batch request body50 MiB
IDs per touch request1000
Documents per sourceDepends on the plan: from 100,000
Open sync60 minutes by default, up to 24 hours

A document above the source's document limit fails with the reason limit_reached in a batch; a single-document request answers 409 with the code sources.push_too_many_documents. Rate limits apply as to any other request; see Rate limits and quotas.

Common errors

AnswerCauseWhat to do
401 sources.push_key_invalidThe key is wrong, revoked or belongs to another source.Check the key and the source ID in the path.
400 sources.push_document_invalidA required field is missing or a limit is exceeded.Fix the document; in a regular batch, only that document fails.
409 sources.push_sync_openAnother sync is still open.Close it, or wait until it expires.
409 sources.push_sync_closedThe sync is closed or has expired.Open a new sync; raise max_minutes for long exports.
403 sources.feature_disabledSources are not part of the organization's plan.Contact the administrator.
429Too many requests.Wait for Retry-After; send fewer, larger batches.