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:
| Method | Path | Purpose |
|---|---|---|
GET | /api/d/{slug}/manifest | Read the folder tree and document inventory |
POST | /api/d/{slug}/folders | Create folder paths (idempotent) |
POST | /api/d/{slug}/upload | Single-request multipart upload (small files) |
POST | /api/d/{slug}/upload/presign | Direct upload step 1: get a presigned PUT URL |
POST | /api/d/{slug}/upload/complete | Direct upload step 2: ingest the staged file |
GET | /api/health | Service 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.
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.
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.
/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 formatfolderPathaccepts on uploads.versionNumber,sizeBytes,sha256,mimeType,scanStatusanduploadedAtdescribe the current (highest) version and arenullfor 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.
/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).
| Field | Type | Required | Description |
|---|---|---|---|
paths | string[] | yes | 1–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.
/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.
| Field | Type | Required | Description |
|---|---|---|---|
file | file | yes | The file to upload (non-empty) |
folderPath | string | no | Target folder path, e.g. Financial/Q3. Missing folders are created automatically. Defaults to API-uploads. |
note | string | no | Version 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.
/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:
POST …/upload/presignwith the file name and size → presigned URL;PUTthe file bytes to that URL, with exactly the returned headers;POST …/upload/completeto 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).
| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | yes | 1–500 characters; must have an allowed extension |
sizeBytes | integer | yes | Exact 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 toupload/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
/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.
| Field | Type | Required | Description |
|---|---|---|---|
key | string | yes | Staging key from the presign response (1–200 chars) |
fileName | string | yes | 1–500 characters; the name the document gets |
folderPath | string | no | Target folder path (max 1000 chars); missing folders are created. Defaults to API-uploads. |
note | string | no | Version 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.
/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" }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>" }
| Status | When |
|---|---|
400 Bad Request | Malformed 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 Unauthorized | Missing, malformed, unknown, or revoked API key |
403 Forbidden | The administrator who issued the key is no longer an active admin of this dataroom |
404 Not Found | Unknown dataroom slug |
409 Conflict | The dataroom is archived (read-only); write endpoints only |
429 Too Many Requests | Rate limit reached (see Limits) |
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"
}| Code | Meaning |
|---|---|
invalid_name | File name empty or invalid after sanitisation |
empty_file | Zero-byte file |
too_large | File exceeds 500 MB |
type_not_allowed | Extension not on the allowlist (message names the extension) |
content_mismatch | File contents (magic bytes) do not match the extension |
malware | Malware scanner rejected the file |
storage_limit | The subscription's storage limit is reached; existing documents stay available |
billing_blocked | The 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.
Limits
All limits below are enforced server-side.
| Limit | Value |
|---|---|
Uploads (classic upload, upload/presign and folders calls combined) | 300 per hour per key (rolling 1-hour window) |
upload/complete | Not counted (its presign already was) |
| Manifest reads | 120 per hour per key (separate window) |
| Maximum file size | 500 MB (524 288 000 bytes) |
Multipart request body (classic upload) | ± 5 MB practical ingress limit — use the direct upload beyond that |
| Presigned PUT URL validity | 900 seconds (15 minutes) |
paths per folders call | 200 |
fileName length | 500 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:
| Category | Extensions |
|---|---|
pdf | |
| Images | png, jpg, jpeg, webp |
| Office (modern) | docx, xlsx, pptx |
| Office (legacy) | doc, xls, ppt |
| Plain text | txt, 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.
Changelog
| Date | Change |
|---|---|
| 2026-08-26 | Initial publication of this manual. Documents the dataroom API as shipped: manifest, folders, upload (multipart), upload/presign, upload/complete, health. |