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 is clearly visible, then the same thing
in C#.
It's intended for 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 | Behavior |
|---|---|
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.
A pagination token belongs to the exact query that produced it. If the search criteria change between pages — for example, because the user kept typing in a search field — the old token is invalid and the response will be
400 VALIDATION_FAILED. In that case, start the search over without acontinuationToken. The same applies to document lists: a token from one case file is not valid in another, or in a different section. Sending a criterion that is empty or contains only whitespace will also be rejected with400— to page through everything, simply omit the parameter.
Listing 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
Uploading happens in four steps, because the file bytes don't flow through the API itself: request a staging slot, put the file there, trigger the import, then retrieve the result.
Step 1: Request a Staging 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: Put 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 request goes directly to the storage, 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,
which uploads the file in parts and assembles them 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 confirms. 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 DMS may deny access, or the import may time out. An integration that stops after the202will report successes that aren't real.
The
operationIdis a 32-character hexadecimal identifier — not a format you choose yourself. It is assigned by the API and used as-is.
Step 4: Wait for the Result
You can append the statusUrl from the response directly to
$BASE — that's exactly why $BASE does
not include the version, and each path carries 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 in progress:
{ "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.
When 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 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 sending again: blindly retrying can result in a duplicate document. The service does not guess the outcome and does not replay a previous response.
The retrieval window is seven days. After that, the operation URL responds with
404.
A reasonable 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 complete, the operation query returns the download URL:
{
"operationId": "op_2d90ff41ab",
"status": "succeeded",
"result": {
"downloadUrl": "https://...s3.eu-central-1.amazonaws.com/...&X-Amz-Signature=...",
"downloadUrlExpiresAt": "2026-09-09T10:12:24Z",
"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 — until the moment specified in
result.downloadUrlExpiresAt, roughly 15 minutes. It should not be cached; instead, call the operation query again to get a fresh one.Do not confuse this with
transferDeadline. That is the deadline for the operation itself and only indicates how long you can poll for it; it falls much later and does not apply to the download URL.
To find out which versions and renditions exist for a document, use the read endpoints:
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 is filed in, 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 downloaded file's name is based on the rendition, not the document:
the PDF preview of a Vertrag.docx arrives as
Vertrag.pdf. The rendition field in the result
shows the requested name. If the rendition doesn't exist, the operation
query 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 importing: 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 used a second time.
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; the operation query 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 no comment is provided,
the comment from the previous version is carried over. By default, the
document takes on the new file name; set "keepFileName": true
to retain the existing one. The fileName must — as with
importing — include a file extension, otherwise the call responds with
400. An unknown document results in failed
with errorCode: "NOT_FOUND".
A document currently being edited by someone else will not be overwritten. If it is checked out, 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 target case file and target section are validated upfront. If the
case file doesn't exist, or doesn't have the specified section, the
response is 404 NOT_FOUND and the message specifies what is
missing — not a server error that would invite a retry.
targetTrayId may be 0: this files the document
directly in the case file, without a section.
The Same in C#
A minimal client that searches for a case file and imports a document. The access token is assumed to be available, as it would be obtained during sign-in.
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 a case file
var search = await http.GetFromJsonAsync<SearchResponse>(
"v1/dms/documents/container?searchTerm=Mustermann&pageSize=25");
var container = search!.Containers[0];
// 2) Request a staging 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 timeout, otherwise the loop runs
// indefinitely 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(
$"Operation {accepted.OperationId} still pending after 10 minutes.");
}
}
Console.WriteLine(status.Status == "succeeded"
? $"Imported as {status.Result!.Value.GetProperty("documentId")}"
: $"Failed: {status.ErrorCode} - {status.Reason}");The four data classes used:
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 binds the names automatically because the API
responds in camelCase and the default comparison is case-insensitive.
In a production integration, two additional things are needed 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. Sending the same
Idempotency-Keyagain causes the API to respond with409 IDEMPOTENCY_KEY_REUSEDor504 IDEMPOTENCY_KEY_INFLIGHT_TIMEOUT— the request will not be executed a second time, but its outcome will not be returned either. The application must look that up via the operation query.
Further Reading
This article has been automatically translated by an AI and may therefore contain errors.