Documentation / Developers

API reference

Authenticate and push documents from your own systems.

The REST API lets your own systems push documents into docuflow-ai, read them back and maintain the reference data they need. All requests and responses use JSON. An interactive OpenAPI (Swagger) console is available at https://app.docuflow-ai.com/swagger.

Base URL#

https://app.docuflow-ai.com

All endpoints below are relative to this address. Always use HTTPS. Responses use camelCase property names; property names in request bodies are not case-sensitive.

Authentication#

Every request must carry your organization's API credentials in two headers. Copy them from Admin → Organization Management → Initial Setup.

HeaderValue
X-Api-KeyYour organization's API key. It identifies the organization the request acts on.
X-Api-SecretYour organization's API secret.
curl https://app.docuflow-ai.com/api/tenant \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET"
Keep the secret secret
Call the API from server-side code only. Never embed the secret in a browser app, a mobile app or a public repository. Requests with missing or wrong credentials get 401 Unauthorized.

Endpoints#

MethodPathPurpose
GET/api/tenantOrganization name and storage path template.
GET/api/content-typesAccepted file types and their ids.
POST/api/documentsUpload a file (Base64).
POST/api/documents/recordRegister a file already in organization storage.
POST/api/documents/record/bulkRegister up to 500 stored files at once.
GET/api/documentsThe latest version of every document.
GET/api/documents/{id}One document with all its metadata.
GET/api/documents/{id}/statusWhere a document is in processing.
DELETE/api/documents/{id}Delete a document and its text.
POST/api/documents/mergeMerge several PDFs into one document.
GET/api/documents/{id}/originalDownload the stored file.
GET/api/documents/{id}/pdfDownload the document as PDF.
GET/api/documents/{id}/textThe text OCR read out of the file.
GET/api/documents/{id}/jsonThe structured data extracted from it.
POST/api/documents/{id}/extractQueue a fresh extraction.
PUT/api/documents/{id}/tagsReplace the document's tags.
GET/api/documents/{id}/versionsEvery version of the document.
GET/api/documents/{id}/jobsAI jobs that ran on the document.
GET/api/documents/duplicatesDocuments stored more than once.
POST/api/documents/searchKeyword or semantic search.
GET/api/document-typesThe document types and their schemas.
GET/api/document-types/{id}One document type with its schema.
POST/api/document-typesAdd a document type and index it.
PUT/api/document-types/{id}Change a document type or its schema.
DELETE/api/document-types/{id}Deactivate a document type.
GET/api/document-types/{id}/fieldsFields its index can be queried on.
GET/api/document-types/{id}/index-analysisIndex health and document counts.
POST/api/document-types/{id}/reindexIndex the type's documents again.
POST/api/document-types/{id}/recreate-indexRebuild the index from the schema.
POST/api/search/nlAsk a question of a document type.
GET/api/jobsThe most recent AI jobs.
GET/api/jobs/{requestId}Status and result of one job.
DELETE/api/jobs/{requestId}Cancel a job that has not finished.
DELETE/api/jobs?status=queuedRemove every job in one status.
GET/api/jobs/failuresRecords held back after repeated failures.
POST/api/jobs/failures/{id}/requeueLet a failed record be retried.
GET/api/tagsThe tags documents can be labelled with.
GET/api/tags/{id}One tag with its allowed values.
POST/api/tagsDefine a new tag.
PUT/api/tags/{id}Change a tag or its values.
DELETE/api/tags/{id}Deactivate a tag.
GET/api/tenant/creditsCredit balance and low-credit threshold.
GET/api/tenant/transactionsWhat credit was spent and bought.
GET/api/link-collection-schedulesCollection schedules for batch runners.

Get organization#

GET/api/tenant

Returns the organization the credentials belong to. defaultFilePathTemplate is the folder where the organization's files are stored; use it as filePath for the record endpoints.

200 OK
{
  "tenantName": "Contoso Ltd",
  "defaultFilePathTemplate": "\\DocuFlowFiles\\Tenant_3e9d7\\Documents"
}

List content types#

GET/api/content-types

Returns the active content types. Pass a contentTypeId from this list when you upload; an inactive or unknown id is rejected.

Content types are shared by every organization on the platform, so the list is read-only through the API. Platform administrators maintain it under Admin → System administration → Content Types.

200 OK
[
  { "contentTypeId": 1, "contentTypeName": ".pdf" },
  { "contentTypeId": 3, "contentTypeName": ".docx" },
  { "contentTypeId": 28, "contentTypeName": ".png" }
]

Upload a document#

POST/api/documents

Stores the file and queues it for processing. The response returns as soon as the file is saved; processing continues in the background.

Body fieldTypeDescription
documentName requiredstringFile name without extension.
documentExtensionstringExtension including the dot, e.g. .pdf. PDFs are split automatically.
contentTypeIdintegerId from /api/content-types.
documentContent requiredstringThe file content, Base64 encoded.
curl -X POST https://app.docuflow-ai.com/api/documents \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "{
        \"documentName\": \"invoice-2026-0042\",
        \"documentExtension\": \".pdf\",
        \"documentContent\": \"$(base64 -w0 invoice-2026-0042.pdf)\"
      }"
201 Created
{
  "documentId": "<generated id>",
  "documentName": "invoice-2026-0042",
  "documentExtension": ".pdf",
  "contentTypeId": 1,
  "versionNo": 1
}

Register a stored file#

POST/api/documents/record

For integrations that write files straight into the organization's storage folder (for example the DocuRunner batch runner). No file content is sent; docuflow-ai reads the file from storage and queues it for OCR (PDFs go through splitting first).

File naming
filePath is the folder that holds the file, and the file itself must be named <documentId><documentExtension>, e.g. portal-000123.pdf. Write the file first, then register it.
Body fieldTypeDescription
documentId requiredstring (≤ 30)Your unique id for the document. Must not exist yet.
documentName requiredstringFile name without extension.
documentExtensionstringExtension including the dot.
contentTypeIdintegerId from /api/content-types.
filePath requiredstring (≤ 500)Folder that contains the file, normally based on defaultFilePathTemplate from /api/tenant.
sourceKeystring (≤ 20)Identifies the source system or collection the file came from.
curl -X POST https://app.docuflow-ai.com/api/documents/record \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
        "documentId": "portal-000123",
        "documentName": "statement-august",
        "documentExtension": ".pdf",
        "filePath": "\\DocuFlowFiles\\Tenant_3e9d7\\Documents",
        "sourceKey": "supplier-portal"
      }'

Returns 201 Created with the same body as an upload, or 409 Conflict if the documentId already exists.

Register stored files in bulk#

POST/api/documents/record/bulk

Accepts a JSON array of up to 500 record objects (same fields as above) and registers them in a single request. The whole request is rejected if any record is invalid, if ids repeat inside the request, or if any id already exists.

201 Created
{
  "insertedCount": 2,
  "documents": [
    { "documentId": "portal-000001", "documentName": "statement-august", "documentExtension": ".pdf", "contentTypeId": null, "versionNo": 1 },
    { "documentId": "portal-000002", "documentName": "statement-september", "documentExtension": ".pdf", "contentTypeId": null, "versionNo": 1 }
  ]
}
400 Bad Request
{
  "errors": [
    { "index": 1, "error": "FilePath is required." }
  ]
}

List documents#

GET/api/documents

Returns the latest version of every document in your organization. The response is not paged, so for large organizations expect a big payload and cache what you can.

FieldTypeDescription
documentIdstringId of this document version.
documentNamestringFile name without extension.
documentExtensionstringExtension including the dot.
contentTypeIdintegerId from /api/content-types.
documentTypeIdstringThe document type the AI classified it as, when there is one.
documentStatusIdintegerProcessing status.
versionNointegerVersion number; each version is a separate documentId.
documentSizeintegerFile size in bytes.
pageCountintegerPages found during processing; 0 until then.
sourceKeystringSource system or collection the file came from.
parentDocumentIdstringSet on split or merged documents, pointing at the document they came from.
tagsstringTags assigned in the app.
createdDatestringUTC timestamp of the upload.
200 OK
[
  {
    "documentId": "portal-000123",
    "documentName": "statement-august",
    "documentExtension": ".pdf",
    "contentTypeId": 1,
    "documentTypeId": "inv",
    "documentStatusId": 30,
    "versionNo": 1,
    "documentSize": 184320,
    "pageCount": 3,
    "sourceKey": "supplier-portal",
    "parentDocumentId": null,
    "tags": null,
    "createdDate": "2026-08-31T09:14:22"
  }
]

Get a document#

GET/api/documents/{id}

Returns one document with the fields above, or 404 Not Found. The id is the one you got back from an upload or chose yourself when registering a stored file. Metadata only — use Download in the app for the file itself.

Check processing status#

GET/api/documents/{id}/status

The cheap way to poll after an upload. isOcrCompleted turns true once the document reaches the status configured in OcrCompletedStatusId (25 by default), which is the point where its text is searchable. Poll every few seconds at most; OCR of a large PDF takes minutes.

curl https://app.docuflow-ai.com/api/documents/portal-000123/status \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET"
200 OK
{
  "documentId": "portal-000123",
  "documentStatusId": 30,
  "documentStatusName": "Indexed",
  "isOcrCompleted": true
}

Delete a document#

DELETE/api/documents/{id}
This cannot be undone
The document row, its extracted text and JSON, and — when Store documents is on — the stored file are all removed for good. There is no recycle bin.

Deletes exactly the version you name; other versions of the same document keep their own ids and stay. Returns 204 No Content, or 404 Not Found if the id is unknown.

Merge documents#

POST/api/documents/merge

Joins two or more PDFs into a single new document, in the order the ids are given. The merged document enters processing like a freshly uploaded PDF and becomes the parent of the documents it was built from, which keep their own ids and stay where they are.

Body fieldTypeDescription
documentIds requiredstring[]At least 2 ids, at most 500. Every one must be a PDF in your organization.
documentName requiredstringName for the merged document, without extension.
curl -X POST https://app.docuflow-ai.com/api/documents/merge \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
        "documentIds": ["portal-000001", "portal-000002"],
        "documentName": "statement-q3"
      }'

Returns 201 Created with the new document. 400 when fewer than two ids are given or any of them is not a PDF, 404 when an id is unknown (the body lists them).

Download the original file#

GET/api/documents/{id}/original

Streams the stored file back exactly as it was uploaded, with the media type of its extension and a Content-Disposition filename. 404 when the id is unknown or the file is no longer stored (Store documents off).

Download a PDF rendition#

GET/api/documents/{id}/pdf

The same file converted to PDF — for Word, Excel and PowerPoint documents, for instance. A PDF is returned as it is. Answers 409 Conflict when the file's type cannot be rendered as PDF at all, so check for that rather than assuming every document has a rendition.

Get the extracted text#

GET/api/documents/{id}/text

The plain text OCR read out of the document: {"documentId": "...", "text": "..."}. Answers 404 while OCR has not finished, so poll /status until isOcrCompleted is true before asking for it.

Get the extracted JSON#

GET/api/documents/{id}/json

The structured data the AI extracted, returned as JSON itself rather than as a string holding JSON — parse the body directly. Its shape follows the document type the document was classified as. 404 when nothing has been extracted yet.

200 OK
{
  "invoiceNumber": "A-2026-0042",
  "invoiceDate": "2026-03-01",
  "supplier": { "name": "Northwind Ltd", "vatNumber": "GB123456789" },
  "lineItems": [
    { "description": "Consulting", "quantity": 4, "unitPrice": 250.00, "total": 1000.00 }
  ],
  "total": 1000.00
}

Re-run extraction#

POST/api/documents/{id}/extract

Queues the document for a fresh extraction, which overwrites the JSON above when it finishes. No body. Answers 202 Accepted with {"requestId": "..."}; read /api/jobs/{requestId} for the outcome. 409 when the document has no readable PDF to analyse.

Extraction costs credit
Every run is charged to your credit balance.

Replace a document's tags#

PUT/api/documents/{id}/tags

Sets the document's tags to exactly what you send: a tag left out of the body is cleared. Each tag name must be one defined under /api/tags, and each value must be one that tag allows. A tag of type S takes a single value; a tag of type M takes several.

cURL
curl -X PUT https://app.docuflow-ai.com/api/documents/portal-000001/tags \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
        "tags": {
          "Department": ["Finance", "Legal"],
          "Year": ["2026"]
        }
      }'

Returns 200 OK with the updated document. 400 names the offending tag when it is not defined, the value is not allowed, a single-value tag was given several, or a required tag has no value.

List a document's versions#

GET/api/documents/{id}/versions

Every version sharing the document's name and extension, oldest versionNo first. Each version is its own document with its own id; this is how you find the others from any one of them.

List a document's jobs#

GET/api/documents/{id}/jobs

The AI jobs that ran on this document, newest first, in the shape described under jobs.

List duplicate documents#

GET/api/documents/duplicates

Documents whose content is byte-for-byte identical to another one's — matched on checksum and size — so you can decide which to keep. Both sides of each pair are listed.

Search documents#

POST/api/documents/search
Body fieldTypeDescription
query requiredstringWhat to search for.
modestringkeyword (the default) matches words; semantic matches meaning.
sizeintegerHits to return, 1–100. Defaults to 10.
skipintegerHits to skip, for paging. Defaults to 0.
tagsstring[]Keeps only documents carrying every one of these tag values.
documentTypeIdstringKeeps only documents of this type.

highlights maps each matching index field to fragments with the match wrapped in <em>; semantic hits carry a score but no highlights.

Filters are applied to the results
tags and documentTypeId narrow the hits the search returned rather than the search itself, because neither is held in the search index. A size of 10 can therefore come back with fewer than 10 documents once a filter is applied — raise size rather than assuming there are no more.
curl -X POST https://app.docuflow-ai.com/api/documents/search \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
        "query": "overdue invoice",
        "mode": "keyword",
        "size": 10,
        "tags": ["Finance"]
      }'
200 OK
[
  {
    "documentId": "portal-000042",
    "documentName": "invoice-2026-0042",
    "documentExtension": ".pdf",
    "documentTypeId": "invoice0001",
    "documentStatusId": 40,
    "tags": "Department, Finance",
    "score": 8.42,
    "highlights": {
      "documentContent": ["this <em>invoice</em> is <em>overdue</em> as of"]
    },
    "createdDate": "2026-03-01T09:12:44Z"
  }
]

List document types#

GET/api/document-types

The document types defined for your organization. Each one owns a schema describing the data extracted from documents of that type, and its own search index. documentTypeJson is left out here — read a single type for it.

200 OK
[
  {
    "documentTypeId": "invoice0001",
    "documentTypeName": "Invoice",
    "industryNames": "Finance, Legal",
    "isIndexCreated": true,
    "indexName": "cldoc_invoice0001_acme",
    "isActive": true,
    "documentTypeJson": null
  }
]

Get a document type#

GET/api/document-types/{id}

The same fields plus documentTypeJson, the type's JSON Schema. 404 when the id is unknown.

Create a document type#

POST/api/document-types
Body fieldTypeDescription
documentTypeName requiredstring (≤ 100)What the type is called, e.g. Invoice.
documentTypeJsonstringThe schema, as JSON. A sample of the data itself is accepted too and is converted into the schema that describes it.
isActivebooleanDefaults to true.

The search index is created from the schema right after the type is, so a type sent without documentTypeJson is created without an index — add the schema later to get one. Returns 201 Created, or 400 when documentTypeJson is not valid JSON.

Update a document type#

PUT/api/document-types/{id}

Replaces the whole type, so send every field you want to keep. When the type already has an index, the schema change is applied to it as part of the call. Sending isActive: false is how a type is retired — documents already filed under it keep their classification.

Some schema changes need the index rebuilt
A search index can gain a field but cannot lose one or change a field's type in place. Adding a field to the schema therefore takes effect at once, while removing one or changing its type does not — call recreate-index to put those into effect, and index-analysis to see what the index holds now.
200 OK
{
  "documentTypeId": "invoice0001",
  "documentTypeName": "Invoice",
  "industryNames": "Finance",
  "isIndexCreated": true,
  "indexName": "cldoc_invoice0001_acme",
  "isActive": true,
  "documentTypeJson": "{ \"$schema\": \"...\", \"type\": \"object\", ... }"
}

Delete a document type#

DELETE/api/document-types/{id}

Deactivates the type rather than erasing it — the same as sending isActive: false. Its index is left in place, so documents already filed under it keep their classification and stay searchable. Only your organization's own types can be deleted. Returns 204 No Content, or 404 Not Found when the id is not one of yours.

List a document type's index fields#

GET/api/document-types/{id}/fields

The fields the type's index holds, each with its search type: [{"name": "invoice.total", "type": "double"}]. These are the fields a natural language question can be answered from. A type with no index yet returns an empty list.

Analyze a document type's index#

GET/api/document-types/{id}/index-analysis

The index's health and size alongside how far the type's documents have got: how many are searchable, how many are still waiting and how many are not ready. The place to look when a search returns less than you expect.

200 OK
{
  "indexName": "cldoc_invoice0001_acme",
  "indexExists": true,
  "isIndexCreated": true,
  "indexedDocumentCount": 1284,
  "storeSizeBytes": 10485760,
  "primaryStoreSizeBytes": 5242880,
  "health": "green",
  "status": "open",
  "primaryShards": 1,
  "replicas": 1,
  "totalDocuments": 1300,
  "searchableDocuments": 1284,
  "waitingDocuments": 16,
  "notReadyDocuments": 0
}

Reindex a document type#

POST/api/document-types/{id}/reindex

Puts the type's documents back through indexing against the current schema, keeping the index itself. No body. Use it after a schema change that only added fields. The response carries status, success, indexName and affectedDocuments; indexing then runs in the background.

Recreate a document type's index#

POST/api/document-types/{id}/recreate-index

Drops the index, builds it again from the current schema and re-indexes the type's documents — the way to apply a removed or re-typed field. Same response as reindex.

Documents are not searchable meanwhile
Until the documents have been indexed again they will not answer searches on this type.

Ask a question in natural language#

POST/api/search/nl

Asks a question of one document type's index in plain language: the model turns it into a search, runs it, and answers with the query it built, the ids it matched and a one-line summary. Body is {"documentTypeId": "...", "question": "..."}.

Answers 202 Accepted with {"requestId": "..."} — the answer is not ready yet. Poll /api/jobs/{requestId} until status is Completed; result then holds the JSON below. 409 when the document type has no index yet.

curl -X POST https://app.docuflow-ai.com/api/search/nl \
  -H "X-Api-Key: $DOCUFLOW_API_KEY" \
  -H "X-Api-Secret: $DOCUFLOW_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
        "documentTypeId": "invoice0001",
        "question": "which invoices from Northwind are over 1000 EUR?"
      }'
The completed job's result
{
  "query": { "bool": { "filter": [ { "term": { "supplier.name": "Northwind Ltd" } } ] } },
  "documentIds": ["portal-000042", "portal-000051"],
  "summary": "Two Northwind invoices above 1000 EUR."
}
Questions cost credit
Each question is charged to your credit balance.

List jobs#

GET/api/jobs?limit=50

Your organization's most recent AI jobs, newest first. limit is 1–500 and defaults to 50; anything outside that is 400.

FieldTypeDescription
requestIdstringThe id the queueing endpoint returned. Use it to read one job.
jobIdstringThe processing engine's own id, for support requests.
documentIdsstringComma-separated ids of the documents the job ran over, when it ran over any.
functionNamestringWhat kind of job it was, e.g. Search or RecreateJson.
statusstringqueued, Completed or Failed.
resultstringThe answer once it finished; null while it is still queued. Carries the error message on a failure.
createdDatestringUTC timestamp of when the job was queued.

Get a job#

GET/api/jobs/{requestId}

One job by the requestId a queueing endpoint gave you — this is where the answer to a queued search or extraction turns up. 404 when the id is unknown. Poll every few seconds at most.

200 OK
{
  "requestId": "6f1c2f2e8a614d0f9b3c7a1d5e0b2c48",
  "jobId": "rq:job:0c7f1b2a",
  "documentIds": "portal-000042",
  "functionName": "RecreateJson",
  "status": "Completed",
  "result": "{ \"invoiceNumber\": \"A-2026-0042\", ... }",
  "createdDate": "2026-03-01T09:12:44Z"
}

Delete a job#

DELETE/api/jobs/{requestId}

Cancels a job that has not finished: it drops out of GET /api/jobs, is taken off the processing queue, and its result is discarded if the model finishes it anyway. Only a pending or queued job can be deleted — a finished one answers 409 Conflict. Returns 204 No Content, or 404 when the id is not one of your organization's jobs. When the job could not be taken off the queue, the answer is 200 OK with queueRemoveFailed: 1; the job is deleted all the same.

Delete jobs by status#

DELETE/api/jobs?status=queued

Deletes every one of your organization's jobs in one status — queued, Completed or Failed; anything else is 400. Queued jobs are also taken off the processing queue. Returns 200 OK with how many jobs were deleted and how many could not be taken off the queue.

200 OK
{
  "deleted": 4,
  "queueRemoveFailed": 0
}

List job failures#

GET/api/jobs/failures

Records that failed their job often enough to be held back from further attempts — a PDF too large for the model, for instance. recordId is the document id for OCR and split failures. lastError says what went wrong on the most recent attempt.

200 OK
[
  {
    "aiJobFailureId": 17,
    "jobType": "OCR",
    "recordId": "portal-000091",
    "failedCount": 3,
    "lastError": "Document exceeds the model context window.",
    "createdDate": "2026-02-27T22:04:10Z",
    "lastModifiedDate": "2026-03-01T02:15:38Z"
  }
]

Retry a job failure#

POST/api/jobs/failures/{id}/requeue

Clears the record's failure history so its job picks it up again with a fresh set of attempts, and puts a document that was parked as failed back into the status the job reads from. No body. Returns 204 No Content, or 404 when the id is not one of your organization's failures.

List tags#

GET/api/tags

The tags documents can be labelled with. allowedValues is the list a document's tag value must come from, and tagType says whether a document may carry one value (S) or several (M).

200 OK
[
  {
    "tagId": "tag0000000000000001",
    "tagName": "Department",
    "tagType": "M",
    "tagDescription": "Owning department",
    "isRequired": false,
    "allowedValues": ["Finance", "Legal", "Operations"],
    "isActive": true
  }
]

Get a tag#

GET/api/tags/{id}

One tag in the shape above, or 404 Not Found.

Create a tag#

POST/api/tags
Body fieldTypeDescription
tagName requiredstring (≤ 50)What the tag is called, e.g. Department.
tagType requiredstringS for a single value per document, M for several.
validationDetails requiredstring (≤ 500)The values the tag allows, one per line.
tagDescriptionstring (≤ 200)Free text shown in the app.
isRequiredbooleanWhen true, every tag assignment must give this tag a value.
isActivebooleanDefaults to true.

Returns 201 Created, or 409 Conflict when the name is already taken.

Update a tag#

PUT/api/tags/{id}

Replaces the whole tag, so send every field you want to keep. Returns 200 OK, 404 when the id is unknown, or 409 when another tag already has that name. Sending isActive: false is how a tag is retired: it stops being offered for new assignments while documents already carrying it are left as they are.

Removing a value does not untag documents
Documents already carrying a value you drop from validationDetails keep it. Their next tag update has to use a value that is still allowed.

Delete a tag#

DELETE/api/tags/{id}

Deactivates the tag rather than erasing it — the same as sending isActive: false. Documents already carrying it keep their values. Only your organization's own tags can be deleted. Returns 204 No Content, or 404 Not Found when the id is not one of yours.

Get the credit balance#

GET/api/tenant/credits

What your organization has left to spend. AI work — OCR, extraction, searches — draws on this balance, so it is worth checking before a large batch. isLowCredit is true once the balance is at or below lowCreditThreshold, which is the point an automatic top-up is charged when one is set up.

200 OK
{
  "creditBalance": 128.45,
  "lowCreditThreshold": 10,
  "isLowCredit": false
}

List credit transactions#

GET/api/tenant/transactions?from&to

Every credit movement, newest first. creditTransactionCode is D for credit spent and C for credit bought or granted; recordId points at what it was for. from and to are dates (2026-01-01) and both are inclusive of the whole day. Either may be left out.

200 OK
[
  {
    "creditTransactionId": "ct000000000000000001",
    "creditTransactionCode": "D",
    "creditTransactionType": "OCR",
    "creditTransactionAmount": 1.25,
    "usdAmount": 0.50,
    "description": "OCR of invoice-2026-0042.pdf",
    "recordId": "portal-000042",
    "createdDate": "2026-03-01T09:13:02Z"
  }
]
GET/api/link-collection-schedules

Returns the schedules defined under Link collections, for batch runners that collect documents on a timetable.

200 OK
[
  {
    "linkCollectionScheduleId": "nightly0001",
    "linkCollectionScheduleName": "Nightly supplier portals",
    "linkCollectionScheduleCron": "0 2 * * *",
    "isEnabled": true
  }
]

Errors#

Successful calls answer 200 OK for reads, 201 Created for the endpoints that create something, 202 Accepted for the ones that queue AI work and 204 No Content for the deletes and the re-queue. Everything else carries a JSON body:

StatusWhen
400 Bad RequestA required field is missing, the content is not valid Base64 or is empty, a content type id is not active, a bulk request is too large or has duplicates, a schema is not valid JSON, a tag or its value is not defined, or a search size / job limit is out of range. The body is {"error": "..."} (bulk validation returns an errors array with the record index).
401 UnauthorizedMissing or invalid X-Api-Key / X-Api-Secret.
404 Not FoundThe document, content type, document type, tag, job or organization does not exist — including when it belongs to another organization, which is reported the same way.
409 ConflictA documentId or tag name is already in use (the body lists the ids), a content type already uses that name, a document cannot be rendered as PDF, or AI work could not be queued.
Queued work answers twice
The endpoints that return 202 Accepted have only queued the work. A 202 says the job was accepted, not that it succeeded — read /api/jobs/{requestId} for the outcome, where a failure arrives as status: "Failed" rather than as an HTTP error.

Integration tips#

  • Check the result in the app: uploaded documents appear on the Documents page under the system user and move through the statuses.
  • Avoid duplicates: identical content is detected by checksum and size and handled by DefaultDuplicateBehaviour.
  • Large files: Base64 adds about a third to the payload size; for very large or very many files, write them to storage and use the record endpoints.
  • No code? Forward email attachments to your capture address instead.
An unhandled error has occurred. Reload 🗙