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
pathof 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
| Key | Starts with | Can do |
|---|---|---|
| Source key | kbsrc_ | 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 scope | kb_ | 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
| Field | Required | Description |
|---|---|---|
external_id | yes | Your stable ID of the document, up to 512 characters. Sending the same ID again replaces the document. |
title | yes | Card title, up to 500 characters. For a file, the file name by default. |
content | yes, unless there is a file | Up to 5 MB. |
content_type | no | html (default), markdown or text. HTML is cleaned of scripts, styles, forms and embedded objects. |
url | no | Link to the document in your system, shown on the card. |
path | no | Folders, as an array (["Support", "Billing"]) or a string ("Support/Billing"), up to 10 levels. |
permissions | no | {"everyone": true}, or {"emails": [...], "groups": [...]}. Used when the source permissions are "as in the source". |
tags | no | Up to 50 tags, added to the tags of the source. |
author_email | no | If this is a user of the organization, they become the owner of the card. |
updated_at | no | When the document changed in your system (RFC 3339). |
metadata | no | Your key-value pairs on the card. See Metadata. |
category | no | Category of the card: the key of a category of the organization. |
verifiers, verify_interval_days | no | Who verifies the card (users by e-mail and groups) and how often, in days. |
upload_id | no | The file of the document from an upload in parts, instead of content. |
attachments | no | Up 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:
| Request | Does |
|---|---|
POST /v1/push/sources/{source_id}/documents | Creates or replaces one document. |
POST /v1/push/sources/{source_id}/documents/batch | Creates 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}
typeisinitialfor the first sync of the source, andincrementalonce a sync has completed. Anincrementalsync before any completed one returnssources.push_initial_required.- Only one sync can be open at a time: a second one returns
409sources.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…"}
]
}
| Field | Meaning |
|---|---|
result | What happened to the document now: created, updated, unchanged, deleted, not_found, error, or skipped (in an atomic batch that was not applied). |
status | Where the document is on its way to a card: pending, processing (the card is written and is being indexed), indexed, failed or removed. |
card_id | The card of the document, once it exists. |
version | A hash of the content and the fields. The same document sent again has the same version and is unchanged. |
error, reason | Why 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"}'status | Effect |
|---|---|
complete | Full 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_incremental | Applies what you sent and archives nothing. |
failed | Archives 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:
- Open with
"type": "incremental". - Push the changed documents, and put the IDs of removed ones into
deletes(below). - 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-Keywith everyPOST. A repeated batch with the same key returns the first result. - Even without the key, a document with the same
external_idnever becomes a second card: it isunchangedorupdated. - 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
failedif you still have its number, and run the sync again.
Limits
| Limit | Value |
|---|---|
| Content of one document | 5 MB |
| File in a single-document request | 100 MB |
| Documents per batch | 500 |
| Batch request body | 50 MiB |
IDs per touch request | 1000 |
| Documents per source | Depends on the plan: from 100,000 |
| Open sync | 60 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
| Answer | Cause | What to do |
|---|---|---|
401 sources.push_key_invalid | The key is wrong, revoked or belongs to another source. | Check the key and the source ID in the path. |
400 sources.push_document_invalid | A required field is missing or a limit is exceeded. | Fix the document; in a regular batch, only that document fails. |
409 sources.push_sync_open | Another sync is still open. | Close it, or wait until it expires. |
409 sources.push_sync_closed | The sync is closed or has expired. | Open a new sync; raise max_minutes for long exports. |
403 sources.feature_disabled | Sources are not part of the organization's plan. | Contact the administrator. |
429 | Too many requests. | Wait for Retry-After; send fewer, larger batches. |