SERVANDUM

API reference

Dataroom API

Programmatic access to a Servandum virtual data room: read the manifest, create folders and upload documents — with the same validation, malware scanning, versioning and audit trail as the web client.

Base URL  https://app.servandum.nl

01

Introduction

The dataroom API lets you keep a Servandum virtual data room in sync with an external system: create the folder structure, upload documents (with automatic versioning), and read a manifest of everything the dataroom currently contains so that you only upload what actually changed.

Every write goes through exactly the same pipeline as the web client: file-name sanitisation, file-type validation (extension allowlist plus magic bytes), malware scanning, PDF conversion for Office and text formats, document versioning, and a full audit-trail entry. There is no way to bypass these steps through the API.

Scope

The API key grants access to these endpoints only:

MethodPathPurpose
GET/api/d/{slug}/manifestRead the folder tree and document inventory
POST/api/d/{slug}/foldersCreate folder paths (idempotent)
POST/api/d/{slug}/uploadSingle-request multipart upload (small files)
POST/api/d/{slug}/upload/presignDirect upload step 1: get a presigned PUT URL
POST/api/d/{slug}/upload/completeDirect upload step 2: ingest the staged file
GET/api/healthService health (no authentication)

{slug}is the dataroom's URL identifier — the same segment you see in the web client's address bar (https://app.servandum.nl/d/{slug}/…).

Other endpoints under /api/d/{slug}/…that you may encounter in the web client (document viewing, folder ZIP download, Q&A export, audit export, access reports) require an interactive browser session and are not part of the key-based API. API keys are write/manifest-only by design: an API key never returns document content.

02

Authentication

Every request (except GET /api/health) must carry a dataroom API key in the Authorization header:

Authorization: Bearer drk_...
  • Keys start with the prefix drk_ followed by 43 URL-safe base64 characters (32 random bytes).
  • A key is scoped to a single dataroom. It does not work on any other dataroom, even within the same organisation.
  • Only a SHA-256 hash of the key is stored server-side. The full key is shown once, at creation. If you lose it, revoke it and create a new one.
  • Each successful authentication updates the key's “last used” timestamp, visible in the dataroom settings.

Obtaining a key

A dataroom administrator creates keys in the web client under Dataroom → Settings → Upload API. Keys can be revoked there at any time; revocation is effective immediately.

All actions performed with a key are attributed, in the audit trail, to the administrator who issued the key; the key's id and name are recorded in the audit details of every event. If that administrator is deactivated or loses admin rights on the dataroom, the key stops working (403).

Archived datarooms

An archived dataroom is read-only. Write endpoints respond 409; the manifest endpoint still works, so a sync client can always see what a closed dataroom contains.

Security guidance

  • Treat the key like a password. Store it in a secret manager or an environment variable (e.g. DATAROOM_API_KEY); never commit it to source control.
  • Use one key per integration or machine, so revocation is surgical and the audit trail tells you which integration did what.
  • Rotate by creating a new key first, switching the integration over, then revoking the old key.

03

Quick start

Set two variables and make your first call — the manifest is a read-only, side-effect-free way to verify that your key works:

export BASE_URL="https://app.servandum.nl"
export DATAROOM_API_KEY="drk_..."   # from Settings → Upload API

curl -sS "$BASE_URL/api/d/{slug}/manifest" \
  -H "Authorization: Bearer $DATAROOM_API_KEY"

Then upload a first (small) file:

curl -sS -X POST "$BASE_URL/api/d/{slug}/upload" \
  -H "Authorization: Bearer $DATAROOM_API_KEY" \
  -F "file=@report.pdf" \
  -F "folderPath=Financial/Q3"
{
  "ok": true,
  "documentId": "7d9a1c9e-4b1f-4c1a-9a44-2f6d3f8a9b10",
  "versionNumber": 1,
  "fileName": "report.pdf",
  "folderPath": "Financial/Q3"
}

Uploading the same file name to the same folder again creates version 2 of the same document — see the versioning note under POST /upload.

04 · Endpoint

GET/api/d/{slug}/manifest

Returns the dataroom's folder tree (including empty folders) and, per document, its path, current version number, size, and SHA-256 checksum. This is the read side of the API: compare checksums first and upload only what actually differs. A blind re-upload of an unchanged file would create a pointless new version, with version notifications and a polluted audit trail.

Manifest reads are not written to the audit trail (no document content is returned), and they also work on archived datarooms. Rate limit: 120 calls per hour per key, in its own window, separate from the upload limit.

curl -sS "$BASE_URL/api/d/{slug}/manifest" \
  -H "Authorization: Bearer $DATAROOM_API_KEY"

Response 200 OK (cache-control: no-store):

{
  "dataroom": {
    "slug": "example-transaction",
    "name": "Example Transaction",
    "status": "active"
  },
  "generatedAt": "2026-08-26T09:30:00.000Z",
  "folders": [
    { "path": "Financial", "folderId": "f6a0…" },
    { "path": "Financial/Q3", "folderId": "1b2c…" }
  ],
  "documents": [
    {
      "path": "Financial/Q3/report.pdf",
      "folderPath": "Financial/Q3",
      "name": "report.pdf",
      "documentId": "7d9a…",
      "versionNumber": 2,
      "sizeBytes": 184230,
      "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "mimeType": "application/pdf",
      "scanStatus": "clean",
      "uploadedAt": "2026-08-25T14:03:12.000Z"
    }
  ]
}
  • folders[].path — slash-separated path from the dataroom root; the same format folderPath accepts on uploads.
  • versionNumber, sizeBytes, sha256, mimeType, scanStatus and uploadedAt describe the current (highest) version and are null for a document without a version.
  • sha256is the hex SHA-256 of the uploaded file's bytes. Equal checksum → skip; different → upload as a new version; absent from the manifest → new document.

05 · Endpoint

POST/api/d/{slug}/folders

Creates one or more folder paths, including any intermediate levels, in a single call. Idempotent: paths that already exist are left untouched and are not counted as created. Use this to lay down an empty template structure before uploading. Each newly created folder produces a folder_created audit event.

Shares the 300-per-hour upload window (one call counts once). Refused on archived datarooms (409).

FieldTypeRequiredDescription
pathsstring[]yes1–200 folder paths, /-separated, e.g. “Financial/Annual accounts”

Each path segment is sanitised the same way as file names (control characters and < > : " | ? * stripped, leading dots removed, max 200 characters per segment). A path that is empty after sanitisation is rejected with 400 and nothing is created — validation happens for the whole batch before any folder is written.

curl -sS -X POST "$BASE_URL/api/d/{slug}/folders" \
  -H "Authorization: Bearer $DATAROOM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"paths": ["Financial/Annual accounts", "Legal"]}'

Response 200 OK:

{
  "ok": true,
  "folders": [
    { "path": "Financial/Annual accounts", "folderId": "a1b2…" },
    { "path": "Legal", "folderId": "c3d4…" }
  ],
  "created": [
    { "path": "Financial/Annual accounts", "folderId": "a1b2…" }
  ],
  "createdCount": 1
}

folders lists every requested path (sanitised) with its folder id, whether it existed already or was just created; created lists only the folders this call actually created, including intermediate levels.

06 · Endpoint

POST/api/d/{slug}/upload

Single-request upload as multipart/form-data. The file passes through validation, malware scanning, PDF conversion, versioning and auditing before the response returns, so the request can take a while for larger files (the server allows up to 300 seconds). Rate limit: 300 uploads per hour per key.

The platform ingress does not reliably deliver request bodies larger than ±5 MB. For anything bigger, use the two-step direct upload (presign + complete), which supports any allowed file up to 500 MB.
FieldTypeRequiredDescription
filefileyesThe file to upload (non-empty)
folderPathstringnoTarget folder path, e.g. Financial/Q3. Missing folders are created automatically. Defaults to API-uploads.
notestringnoVersion note, stored with the document version
curl -sS -X POST "$BASE_URL/api/d/{slug}/upload" \
  -H "Authorization: Bearer $DATAROOM_API_KEY" \
  -F "file=@report.pdf" \
  -F "folderPath=Financial/Q3" \
  -F "note=Q3 figures, final"

Response 200 OK:

{
  "ok": true,
  "documentId": "7d9a1c9e-4b1f-4c1a-9a44-2f6d3f8a9b10",
  "versionNumber": 1,
  "fileName": "report.pdf",
  "folderPath": "Financial/Q3"
}

Rejection 422 Unprocessable Entity — see Errors:

{
  "error": "type_not_allowed",
  "message": "File type .exe is not allowed.",
  "fileName": "setup.exe"
}

Versioning

Documents are matched on folder + file name. Uploading a name that already exists in the target folder creates a new version of that document (versionNumber increments); a new name creates a new document with versionNumber: 1. To avoid accidental versions, consult the manifest first and skip files whose sha256 is unchanged.

07 · Endpoint

POST/api/d/{slug}/upload/presign

Direct upload, step 1. Validates the file name and size up front and returns a short-lived presigned PUT URL to the platform's object storage. The file bytes then go directly to storage, bypassing the ingress body-size limitation — the recommended path for all files, and the only path for files larger than a few megabytes (up to 500 MB).

The full flow:

  1. POST …/upload/presign with the file name and size → presigned URL;
  2. PUT the file bytes to that URL, with exactly the returned headers;
  3. POST …/upload/complete to run the ingest pipeline and create the document version.

Each presign counts as one upload in the 300-per-hour window (the matching complete call is free).

FieldTypeRequiredDescription
fileNamestringyes1–500 characters; must have an allowed extension
sizeBytesintegeryesExact file size in bytes; positive, max 524 288 000 (500 MB)
curl -sS -X POST "$BASE_URL/api/d/{slug}/upload/presign" \
  -H "Authorization: Bearer $DATAROOM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fileName": "annual-report.pdf", "sizeBytes": 52428800}'

Response 200 OK:

{
  "ok": true,
  "key": "staging/3f2c1a2e-…/0f8fad5b-…",
  "url": "https://…object-storage…?X-Amz-Signature=…",
  "headers": { "Content-Type": "application/pdf" },
  "expiresInSeconds": 900
}
  • key — the staging key; pass it verbatim to upload/complete.
  • url — presigned PUT URL, valid for 900 seconds (15 minutes).
  • headers — headers you must send on the PUT exactly as given (the content type and length are part of the signature).

Validation failures return 422 with the standard error body (codes invalid_name, empty_file, too_large, type_not_allowed).

Step 2 — PUT the bytes

Plain HTTP to object storage; no Authorization header — the signature is in the URL:

curl -sS -X PUT "<url from presign>" \
  -H "Content-Type: application/pdf" \
  --data-binary @annual-report.pdf

08 · Endpoint

POST/api/d/{slug}/upload/complete

Direct upload, step 2. Registers a file that was PUT to staging and runs it through exactly the same pipeline as the classic upload: magic-byte check, malware scan, PDF conversion, versioning, audit. The staging object is removed afterwards whether the ingest succeeds or not — a rejected file never lingers in storage, and a staging key cannot be completed twice.

Does not count toward the rate limit (the presign already did). The server allows up to 300 seconds for scanning and conversion of large files.

FieldTypeRequiredDescription
keystringyesStaging key from the presign response (1–200 chars)
fileNamestringyes1–500 characters; the name the document gets
folderPathstringnoTarget folder path (max 1000 chars); missing folders are created. Defaults to API-uploads.
notestringnoVersion note (max 500 chars)
curl -sS -X POST "$BASE_URL/api/d/{slug}/upload/complete" \
  -H "Authorization: Bearer $DATAROOM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "staging/3f2c1a2e-…/0f8fad5b-…",
    "fileName": "annual-report.pdf",
    "folderPath": "Financial",
    "note": "Uploaded via CI"
  }'

Response 200 OK — identical shape to the classic upload:

{
  "ok": true,
  "documentId": "7d9a1c9e-4b1f-4c1a-9a44-2f6d3f8a9b10",
  "versionNumber": 3,
  "fileName": "annual-report.pdf",
  "folderPath": "Financial"
}

Rejection 422 with the standard error body. In addition to the classic upload codes you may see staging_missing: the staging key is unknown, belongs to another dataroom, or the PUT never (fully) arrived — redo the presign + PUT and try again.

09 · Endpoint

GET/api/health

Service health probe. No authentication required.

curl -sS "$BASE_URL/api/health"

200 OK:

{ "status": "ok", "version": "1.2.3" }

503 Service Unavailable (database unreachable):

{ "status": "degraded", "database": "unreachable", "version": "1.2.3" }

10

Errors

Errors are returned as JSON with a matching HTTP status code. There are two body shapes.

Auth / request errors

Shape: { "error": "<human-readable message>" }

StatusWhen
400 Bad RequestMalformed body: not JSON where JSON is expected, not multipart where multipart is expected, missing or invalid fields, invalid or empty folder path, more than 200 paths
401 UnauthorizedMissing, malformed, unknown, or revoked API key
403 ForbiddenThe administrator who issued the key is no longer an active admin of this dataroom
404 Not FoundUnknown dataroom slug
409 ConflictThe dataroom is archived (read-only); write endpoints only
429 Too Many RequestsRate limit reached (see Limits)
The error string in these responses is a human-readable English message (e.g. "Invalid or revoked API key"). Treat the HTTP status code as the machine-readable signal, not the message text — the wording may change without notice.

Upload rejections

422 Unprocessable Entity with a stable machine code and an English message:

{
  "error": "content_mismatch",
  "message": "The file's contents do not match its extension.",
  "fileName": "report.pdf"
}
CodeMeaning
invalid_nameFile name empty or invalid after sanitisation
empty_fileZero-byte file
too_largeFile exceeds 500 MB
type_not_allowedExtension not on the allowlist (message names the extension)
content_mismatchFile contents (magic bytes) do not match the extension
malwareMalware scanner rejected the file
storage_limitThe subscription's storage limit is reached; existing documents stay available
billing_blockedThe environment is read-only due to an outstanding payment
staging_missing(upload/complete only) staging object missing or invalid — redo presign + PUT

Rate-limit responses (429) include a Retry-After header with the number of seconds until the current window expires; the windows are described under Limits.

11

Limits

All limits below are enforced server-side.

LimitValue
Uploads (classic upload, upload/presign and folders calls combined)300 per hour per key (rolling 1-hour window)
upload/completeNot counted (its presign already was)
Manifest reads120 per hour per key (separate window)
Maximum file size500 MB (524 288 000 bytes)
Multipart request body (classic upload)± 5 MB practical ingress limit — use the direct upload beyond that
Presigned PUT URL validity900 seconds (15 minutes)
paths per folders call200
fileName length500 characters (stored name is sanitised to max 200)
folderPath length (upload/complete)1000 characters
note length (upload/complete)500 characters
Request duration (upload, upload/complete)300 seconds server-side

Accepted file types

The extension must match the file's actual content:

CategoryExtensions
PDFpdf
Imagespng, jpg, jpeg, webp
Office (modern)docx, xlsx, pptx
Office (legacy)doc, xls, ppt
Plain texttxt, csv, md

Additionally, the tenant's subscription storage limit applies (storage_limitrejection); this is a property of the customer's plan, not of the API.

12

Changelog

DateChange
2026-08-26Initial publication of this manual. Documents the dataroom API as shipped: manifest, folders, upload (multipart), upload/presign, upload/complete, health.