This article walks through a complete workflow: searching for a case file,
uploading a document, and downloading a document. First using
curl so each step stays visible, then the same thing
in C#.
It's aimed at anyone building their own integration against the API. You'll need a registered application and a valid access token — both are covered in Connecting Your Own Application.
BASE="https://<mandant>.stp-cloud.de/documents/dms-cloud-api/api"
TOKEN="eyJhbGciOi..."Finding a Case File or Folder
Three mutually exclusive search options are available:
| Parameter | Effect |
|---|---|
name |
Exact search by case file name |
fileReference |
Exact search by file reference number |
searchTerm |
Prefix search across both |
| (none of the above) | Page through all case files and folders |
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/container?searchTerm=Mustermann&pageSize=25"{
"containers": [
{
"containerId": "1f0c9d1e-6f3b-4a2c-9d20-6b0f4f8a1c77",
"name": "Mustermann ./. Musterbank",
"fileReference": "2026-0042",
"type": "Akte"
}
],
"totalCount": 1,
"nextContinuationToken": null
}If nextContinuationToken contains a value, there are
more pages. Pass it unchanged as continuationToken in the
next request.
Listing the Documents in a Case File
CONTAINER="1f0c9d1e-6f3b-4a2c-9d20-6b0f4f8a1c77"
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/container/$CONTAINER/documents?pageSize=50"{
"documents": [
{
"documentId": "8a3d2f10-77bc-4de1-9a55-2b6f9ac13d04",
"name": "Klageschrift.pdf",
"documentType": "pdf",
"sizeBytes": 184320,
"modifiedAt": "2026-08-19T14:02:11Z"
}
],
"totalCount": 1,
"nextContinuationToken": null
}The sections of a case file — its subdivisions, into which documents are filed — and their contents can be retrieved in the same way:
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/container/$CONTAINER/filing-tray"
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/container/$CONTAINER/filing-tray/17/documents"Uploading a Document
The upload process has four steps, because the file bytes don't pass through the API itself: request a storage slot, place the file there, trigger the import, then retrieve the result.
Step 1: Request a Storage Slot
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"fileName":"Schriftsatz.pdf"}' \
"$BASE/v1/platform/uploads/single"{
"objectKey": "kanzlei-mustermann/staging/9c1f.../Schriftsatz.pdf",
"putUrl": "https://...s3.eu-central-1.amazonaws.com/...&X-Amz-Signature=...",
"expiresAt": "2026-08-24T10:12:44Z"
}Step 2: Place the File There
You'll need both values from the response in a moment:
PUT_URL="https://...s3.eu-central-1.amazonaws.com/...&X-Amz-Signature=..." # putUrl
OBJECT_KEY="kanzlei-mustermann/staging/9c1f.../Schriftsatz.pdf" # objectKeyThis call goes directly to the storage location, without the access token — the URL is already signed and time-limited:
curl -sS -X PUT --upload-file ./Schriftsatz.pdf "$PUT_URL"For very large files, use the multipart variant at platform/multipart/…
instead of uploads/single, where the file is uploaded in parts
and then assembled at the end.
Step 3: Trigger the Import
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-schriftsatz-2026-0042-001" \
-d '{
"fileName": "Schriftsatz.pdf",
"objectKey": "kanzlei-mustermann/staging/9c1f.../Schriftsatz.pdf",
"containerId": "1f0c9d1e-6f3b-4a2c-9d20-6b0f4f8a1c77",
"trayId": 17,
"comment": "Eingang über Partneranwendung"
}' \
"$BASE/v1/dms/documents"The response is 202 Accepted:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"statusUrl": "/v1/platform/operations/7f3a2b9e4c1d40e8a5b26f0913c47ade"
}
202means accepted, not done. The request has been received and validated by the cloud — that's all this response tells you. Whether the document actually made it into the document management system is only known once the operation result is available. A lot can still go wrong in between: the connector may lose its connection, the document management system may deny access, the import may time out. An integration that stops after the202is reporting successes that may not be real.
The
operationIdis a 32-character hexadecimal identifier, not a format you define yourself. It is assigned by the API and used as-is.
Step 4: Wait for the Result
The statusUrl from the response can be appended directly to
$BASE — which is exactly why $BASE does not
include the version, leaving each path to carry it itself:
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE$STATUS_URL"Or, equivalently, constructed from the operationId:
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/platform/operations/7f3a2b9e4c1d40e8a5b26f0913c47ade"While the operation is still running:
{ "operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade", "status": "pending" }Once complete:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"status": "succeeded",
"result": {
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"importedBytes": 184320
}
}On failure:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"status": "failed",
"reason": "Das Dokumentenmanagement konnte den Auftrag nicht ausführen.",
"errorCode": "TARGET_DMS_FAILURE"
}errorCode may also be null if the connector did not
report a stable code; in that case reason is the only information
available and is not intended for programmatic evaluation.
If the outcome cannot be determined:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"status": "ambiguous",
"reason": "Der Vorgang hat innerhalb seiner Verarbeitungsfrist kein Ergebnis gemeldet.",
"errorCode": "OPERATION_TIMEOUT"
}
ambiguousis not a failure.failedmeans the operation was rejected and did not take place.ambiguousmeans the operation has ended, but it cannot be determined whether the DMS actually carried it out — for example because the processing deadline passed without a result (OPERATION_TIMEOUT), or because an existing result could not be read (OPERATION_RESULT_UNREADABLE). In this case, check the DMS before retrying: blindly resending may create a duplicate document. The service does not guess the outcome and does not repeat an earlier response.
The retrieval window is seven days. After that, the operation URL responds with
404.
A sensible polling interval is roughly once per second for the first ten seconds, then every five seconds after that.
Downloading a Document
The mirror image of the upload:
DOC="b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18"
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/$DOC/content"{
"operationId": "op_2d90ff41ab",
"statusUrl": "/v1/platform/operations/op_2d90ff41ab"
}Once the operation completes, polling it returns the download URL:
{
"operationId": "op_2d90ff41ab",
"status": "succeeded",
"result": {
"downloadUrl": "https://...s3.eu-central-1.amazonaws.com/...&X-Amz-Signature=...",
"sizeBytes": 184320,
"fileName": "Schriftsatz.pdf",
"version": 3,
"rendition": null
}
}DOWNLOAD_URL="https://...s3.eu-central-1.amazonaws.com/...&X-Amz-Signature=..." # result.downloadUrl
curl -sS -o Schriftsatz.pdf "$DOWNLOAD_URL"The download URL is freshly generated on every request and is only valid for a short time. It should not be cached — instead, poll the operation again to get a new one.
The read endpoints tell you which versions and renditions exist for a document:
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/v1/dms/documents/$DOC" # Master data
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/v1/dms/documents/$DOC/versions" # Version history
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/v1/dms/documents/$DOC/renditions" # RenditionsThe master data includes, among other things, which case file and section the document belongs to, and the current version number:
{
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"title": "Rahmenvertrag",
"documentClass": "stp.doc.contract",
"filing": [{ "containerId": "1f0c9d1e-6f3b-4a2c-9d20-6b0f4f8a1c77", "trayId": 17 }],
"latestVersion": 3,
"versionCount": 3,
"sizeBytes": 184320,
"fileName": "Schriftsatz.pdf"
}The /renditions response lists the names expected by
?rendition= when fetching, along with the version they belong to:
{
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"version": 3,
"renditions": [
{ "rendition": "stp.doc.preview", "extension": "pdf", "sizeBytes": 20480 }
],
"totalCount": 1
}This lets you request a specific version using ?version=2, or a
rendition — an alternative representation of the same content,
such as a PDF preview — using ?rendition=:
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/$DOC/content?rendition=stp.doc.preview"The filename of the downloaded file reflects the rendition rather than the
document: the PDF preview of a Vertrag.docx arrives as
Vertrag.pdf. The rendition field in the result contains
the name that was requested. If the rendition doesn't exist, the operation poll
responds with failed and errorCode: "NOT_FOUND".
Renditions can only be retrieved. This API provides no way to upload a custom rendition.
Uploading a New Version of a Document
The process is the same as for an import: first stage the new
file (POST /v1/platform/uploads/single, then PUT to
the signed URL — steps one and two above), then make the call. It targets the
document rather than the document collection, and the objectKey must
be the one for the newly staged file — an already-imported key cannot be reused.
NEW_OBJECT_KEY="kanzlei-mustermann/staging/4b7e.../Schriftsatz.pdf" # objectKey of the new file
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: version-b41e77a0-002" \
-d '{"fileName":"Schriftsatz.pdf","objectKey":"'"$NEW_OBJECT_KEY"'","comment":"Nach Rücksprache korrigiert"}' \
"$BASE/v1/dms/documents/$DOC/versions"This also responds with 202; polling the operation then returns
the new version number:
{
"status": "succeeded",
"result": {
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"version": 4,
"importedBytes": 187001
}
}The case file, section, title, and document class remain unchanged — only the
content is updated. The comment describes the new version; unlike
when importing a new document, it does not become the document
title, and if comment is omitted, the comment from the previous
version is carried over. By default, the document takes on the new filename; use
"keepFileName": true to keep the existing one. The
fileName must include a file extension — as with an import — otherwise
the call responds with 400. An unknown document results in
failed with errorCode: "NOT_FOUND".
A document that someone else is currently editing will not be overwritten. If it is being edited, has been modified in the meantime, is frozen, or is marked for deletion, the operation ends with
errorCode: "CONFLICT"— in that case, re-read the document and retry the call.
Moving a Document
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: move-doc-b41e77a0-001" \
-d '{"targetContainerId":"3b21...","targetTrayId":4}' \
"$BASE/v1/dms/documents/$DOC/move"This response is synchronous:
{
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"previousContainerId": "1f0c9d1e-6f3b-4a2c-9d20-6b0f4f8a1c77",
"previousTrayId": 17,
"currentContainerId": "3b21...",
"currentTrayId": 4
}The Same Thing in C#
A minimal client that searches for a case file and imports a document. The access token is assumed to already be available from the authentication step.
using System.Net.Http.Headers;
using System.Net.Http.Json;
var http = new HttpClient
{
BaseAddress = new Uri("https://<mandant>.stp-cloud.de/documents/dms-cloud-api/api/")
};
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
http.DefaultRequestHeaders.Add("X-Correlation-Id", Guid.NewGuid().ToString("N"));
// 1) Search for case file
var search = await http.GetFromJsonAsync<SearchResponse>(
"v1/dms/documents/container?searchTerm=Mustermann&pageSize=25");
var container = search!.Containers[0];
// 2) Request a storage slot
var slot = await (await http.PostAsJsonAsync(
"v1/platform/uploads/single", new { fileName = "Schriftsatz.pdf" }))
.Content.ReadFromJsonAsync<UploadSlot>();
// 3) Write the file directly to storage - no token needed, the URL is signed
using (var raw = new HttpClient())
using (var content = new StreamContent(File.OpenRead("Schriftsatz.pdf")))
{
(await raw.PutAsync(slot!.PutUrl, content)).EnsureSuccessStatusCode();
}
// 4) Trigger the import - use the same key on every retry
var import = new HttpRequestMessage(HttpMethod.Post, "v1/dms/documents")
{
Content = JsonContent.Create(new
{
fileName = "Schriftsatz.pdf",
objectKey = slot.ObjectKey,
containerId = container.ContainerId,
trayId = 17,
}),
};
import.Headers.Add("Idempotency-Key", "import-schriftsatz-2026-0042-001");
var accepted = await (await http.SendAsync(import))
.Content.ReadFromJsonAsync<AcceptedResponse>();
// 5) Retrieve the result - with a deadline, otherwise the loop runs
// forever if something goes wrong
OperationResponse status;
var deadline = DateTimeOffset.UtcNow.AddMinutes(10);
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(1));
status = (await http.GetFromJsonAsync<OperationResponse>(
$"v1/platform/operations/{accepted!.OperationId}"))!;
if (status.Status != "pending")
{
break;
}
if (DateTimeOffset.UtcNow > deadline)
{
throw new TimeoutException(
$"Vorgang {accepted.OperationId} ist nach 10 Minuten noch pending.");
}
}
Console.WriteLine(status.Status == "succeeded"
? $"Importiert als {status.Result!.Value.GetProperty("documentId")}"
: $"Fehlgeschlagen: {status.ErrorCode} - {status.Reason}");The four data classes used above:
using System.Text.Json;
sealed record SearchResponse(List<ContainerItem> Containers, int? TotalCount,
string? NextContinuationToken);
sealed record ContainerItem(string ContainerId, string Name, string? FileReference,
string? Type);
sealed record UploadSlot(string ObjectKey, string PutUrl, DateTimeOffset ExpiresAt);
sealed record AcceptedResponse(string OperationId, string StatusUrl);
sealed record OperationResponse(string OperationId, string Status, string? Reason,
string? ErrorCode, JsonElement? Result);HttpClient maps the property names automatically because the API
responds in camelCase and the default comparer is case-insensitive.
A production integration would add two things that this example omits for brevity:
-
Retries with exponential backoff for response codes
429,502,503, and504— see Idempotency and Retries. -
Parsing the error envelope to log
codeandsupportCode— see Error Codes.
A retry does not return the original result. If the same
Idempotency-Keyis sent again, the API responds with409 IDEMPOTENCY_KEY_REUSEDor504 IDEMPOTENCY_KEY_INFLIGHT_TIMEOUT— meaning the request will not be executed a second time, but its outcome will not be returned either. The application must determine the outcome by polling the operation.