Dieser Artikel führt einen vollständigen Ablauf durch: Akte suchen,
Dokument hochladen, Dokument herunterladen. Zuerst mit
curl, damit jeder Schritt sichtbar bleibt, danach dasselbe
in C#.
Er richtet sich an alle, die selbst gegen die Schnittstelle entwickeln. Voraussetzung ist eine registrierte Applikation und ein gültiges Zugriffstoken – beides beschreibt Eine eigene Anwendung anbinden.
BASE="https://<mandant>.stp-cloud.de/documents/dms-cloud-api/api"
TOKEN="eyJhbGciOi..."Eine Akte oder einen Ordner finden
Drei Suchvarianten stehen zur Verfügung, die sich gegenseitig ausschließen:
| Parameter | Wirkung |
|---|---|
name |
Exakte Suche über den Aktennamen |
fileReference |
Exakte Suche über das Aktenzeichen |
searchTerm |
Präfixsuche über beides |
| (keiner davon) | Seitenweises Durchblättern aller Akten – mit
containerType=folder stattdessen aller eigenen Ordner |
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
}Enthält nextContinuationToken einen Wert, gibt es
weitere Seiten. Er wird beim nächsten Aufruf unverändert als
continuationToken mitgeschickt.
Ein Seitenzeiger gehört zu genau der Abfrage, die ihn erzeugt hat. Ändert sich das Suchkriterium zwischen zwei Seiten – etwa weil im Suchfeld weitergetippt wurde –, ist der alte Zeiger ungültig und die Antwort ist
400 VALIDATION_FAILED. In dem Fall wird die Suche ohnecontinuationTokenneu begonnen. Dasselbe gilt für die Dokumentlisten: ein Zeiger aus einer Akte gilt nicht in einer anderen und nicht in einem anderen Register. Ein Kriterium mitzuschicken, das leer ist oder nur aus Leerzeichen besteht, wird ebenfalls mit400abgelehnt – wer alles durchblättern möchte, lässt den Parameter weg.
Die Dokumente der Akte auflisten
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": "00_Klageschrift_ArbG_Hagen_CBF219FC.pdf",
"title": "Klageschrift Arbeitsgericht Hagen",
"documentType": "pdf",
"sizeBytes": 184320,
"modifiedAt": "2026-08-19T14:02:11Z",
"letterDate": "2026-08-04T00:00:00+02:00"
}
],
"totalCount": 1,
"nextContinuationToken": null
}Die Registerstruktur der Akte lesen
Die Dokumentliste oben ist flach: sie nennt alle Dokumente der Akte, aber nicht, in welchem Register sie liegen. Die Untergliederung der Akte kommt aus einem eigenen Aufruf:
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/container/$CONTAINER/filing-tray"{
"filingTrays": [
{
"trayId": 17,
"name": "Schriftverkehr",
"displayText": "Schriftverkehr",
"parentTrayId": 0,
"documentCount": 12,
"position": 1
},
{
"trayId": 23,
"name": "Gericht",
"displayText": "Schriftverkehr Gericht",
"parentTrayId": 17,
"documentCount": 5,
"position": 1
}
]
}parentTrayId trägt die Verschachtelung: 0
heißt, das Register hängt direkt an der Akte – im Beispiel
Schriftverkehr. Jeder andere Wert ist die trayId
des übergeordneten Registers, hier hängt Gericht unter
Schriftverkehr. Aus der Liste lässt sich damit der vollständige
Registerbaum aufbauen; eine eigene Abfrage je Ebene ist nicht nötig, die
Antwort enthält alle Register der Akte auf einmal. position
ist die Reihenfolge unter Geschwisterregistern, so wie der Benutzer sie
im Dokumentenmanagement sieht – nach ihr wird sortiert, nicht nach
trayId oder Name. documentCount nennt die Zahl
der Dokumente unmittelbar in diesem Register, ohne die seiner
Unterregister.
Die Dokumente eines einzelnen Registers auflisten
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/container/$CONTAINER/filing-tray/17/documents"Die Antwort hat dieselbe Form wie die Dokumentliste der Akte.
Standardmäßig enthält sie nur die Dokumente, die unmittelbar in diesem
Register liegen. Mit recursive=true kommen die Dokumente
der darunter verschachtelten Register dazu – im Beispiel also auch die
aus Gericht:
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/container/$CONTAINER/filing-tray/17/documents?recursive=true&pageSize=50"Damit lässt sich ein einzelnes Register samt seiner Unterregister übernehmen, statt alle Dokumente der Akte zu holen und hinterher auszusortieren.
trayId=0 ist kein Register, sondern die Ablage direkt in
der Akte: der Aufruf liefert die Dokumente, die in keinem Register
einsortiert sind. recursive hat dort keine Wirkung, und in
der Registerliste taucht 0 folgerichtig nicht auf.
Ein Dokument hochladen
Der Upload läuft in vier Schritten, weil die Dateibytes nicht durch die Schnittstelle selbst laufen: Ablageplatz anfordern, Datei ablegen, Import beauftragen, Ergebnis abholen.
Erstens: Ablageplatz anfordern
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"
}Zweitens: Die Datei dort ablegen
Die beiden Werte aus der Antwort werden gleich noch gebraucht:
PUT_URL="https://...s3.eu-central-1.amazonaws.com/...&X-Amz-Signature=..." # putUrl
OBJECT_KEY="kanzlei-mustermann/staging/9c1f.../Schriftsatz.pdf" # objectKeyDer Aufruf geht direkt an die Ablage, ohne das Zugriffstoken – die Adresse ist bereits signiert und zeitlich befristet:
curl -sS -X PUT --upload-file ./Schriftsatz.pdf "$PUT_URL"Für sehr große Dateien gibt es statt uploads/single die
mehrteilige Variante unter platform/multipart/…, bei der
die Datei in Teilen abgelegt und am Ende zusammengeführt wird.
Drittens: Den Import beauftragen
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"Die Antwort ist 202 Accepted:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"statusUrl": "/v1/platform/operations/7f3a2b9e4c1d40e8a5b26f0913c47ade"
}
202heißt angenommen, nicht erledigt. Der Auftrag ist in der Cloud eingegangen und geprüft – mehr sagt diese Antwort nicht. Ob das Dokument tatsächlich im Dokumentenmanagement angekommen ist, steht erst im Ergebnis des Vorgangs. Dazwischen kann noch einiges schiefgehen: Der Connector kann die Verbindung verlieren, das Dokumentenmanagement kann die Rechte verweigern, der Import kann in eine Zeitüberschreitung laufen. Eine Integration, die nach dem202aufhört, meldet Erfolge, die keine sind.
Die
operationIdist eine 32-stellige Hexadezimalkennung, kein selbst gewähltes Format. Sie wird von der Schnittstelle vergeben und unverändert weiterverwendet.
Viertens: Auf das Ergebnis warten
Die statusUrl aus der Antwort lässt sich direkt an
$BASE hängen – genau dafür trägt $BASE die
Version nicht und jeder Pfad sie selbst:
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE$STATUS_URL"Oder, gleichbedeutend, aus der operationId
zusammengesetzt:
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/platform/operations/7f3a2b9e4c1d40e8a5b26f0913c47ade"Solange der Vorgang läuft:
{ "operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade", "status": "pending" }Nach Abschluss:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"status": "succeeded",
"result": {
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"importedBytes": 184320
}
}Bei einem Fehlschlag:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"status": "failed",
"reason": "Das Dokumentenmanagement konnte den Auftrag nicht ausführen.",
"errorCode": "TARGET_DMS_FAILURE"
}errorCode kann auch null sein, wenn der
Connector keinen stabilen Code gemeldet hat; reason ist
dann die einzige Auskunft und nicht für eine Auswertung im Programm
gedacht.
Wenn sich der Ausgang nicht feststellen lässt:
{
"operationId": "7f3a2b9e4c1d40e8a5b26f0913c47ade",
"status": "ambiguous",
"reason": "Der Vorgang hat innerhalb seiner Verarbeitungsfrist kein Ergebnis gemeldet.",
"errorCode": "OPERATION_TIMEOUT"
}
ambiguousist kein Fehlschlag.failedheißt: der Vorgang wurde abgelehnt und hat nicht stattgefunden.ambiguousheißt: der Vorgang ist beendet, aber es lässt sich nicht feststellen, ob das DMS ihn ausgeführt hat — etwa weil die Verarbeitungsfrist ohne Ergebnis ablief (OPERATION_TIMEOUT) oder ein vorhandenes Ergebnis nicht lesbar ist (OPERATION_RESULT_UNREADABLE). Prüfen Sie dann im DMS nach, bevor Sie erneut senden: ein blindes erneutes Senden kann ein Dokument doppelt anlegen. Der Dienst rät den Ausgang nicht und wiederholt keine frühere Antwort.
Der Abrufzeitraum beträgt sieben Tage. Danach antwortet die Vorgangs-Adresse mit
404.
Ein sinnvoller Abfragerhythmus ist etwa jede Sekunde für die ersten zehn Sekunden, danach alle fünf Sekunden.
Ein Dokument herunterladen
Spiegelbildlich zum 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"
}Nach Abschluss liefert die Vorgangsabfrage die Download-Adresse:
{
"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"Die Download-Adresse wird bei jeder Abfrage neu erzeugt und ist nur kurz gültig – bis zu dem Zeitpunkt, den
result.downloadUrlExpiresAtnennt, in der Größenordnung von 15 Minuten. Sie sollte nicht zwischengespeichert werden – stattdessen die Vorgangsabfrage erneut aufrufen.Nicht zu verwechseln mit
transferDeadline. Das ist die Frist des Vorgangs selbst und sagt nur, wie lange abgefragt werden darf; sie liegt deutlich später und gilt nicht für die Download-Adresse.
Welche Versionen und Renditions es zu einem Dokument gibt, sagen die Lese-Endpunkte:
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/v1/dms/documents/$DOC" # Stammdaten
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/v1/dms/documents/$DOC/versions" # Versionsverlauf
curl -sS -H "Authorization: Bearer $TOKEN" "$BASE/v1/dms/documents/$DOC/renditions" # RenditionsDie Stammdaten nennen unter anderem, in welcher Akte und welchem Register das Dokument liegt, und die Nummer der aktuellen Version:
{
"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"
}Die Antwort von /renditions nennt die Namen, die
?rendition= beim Abruf erwartet – und die Version, zu der
sie gehören:
{
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"version": 3,
"renditions": [
{ "rendition": "stp.doc.preview", "extension": "pdf", "sizeBytes": 20480 }
],
"totalCount": 1
}Damit lässt sich eine bestimmte Version über ?version=2
anfordern, eine Rendition – eine alternative
Darstellung desselben Inhalts, etwa die PDF-Vorschau – über
?rendition=:
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/dms/documents/$DOC/content?rendition=stp.doc.preview"Der Name der heruntergeladenen Datei richtet sich dann nach der
Rendition, nicht nach dem Dokument: die PDF-Vorschau eines
Vertrag.docx kommt als Vertrag.pdf an.
rendition im Ergebnis nennt den angeforderten Namen. Gibt
es die Rendition nicht, antwortet die Vorgangsabfrage mit
failed und errorCode: "NOT_FOUND".
Renditions lassen sich nur abrufen. Diese Schnittstelle bietet keinen Weg, eine eigene Rendition zu hinterlegen.
Eine neue Version eines Dokuments einstellen
Genau wie beim Import: erst die neue Datei
bereitstellen (POST /v1/platform/uploads/single, dann
PUT auf die signierte Adresse – Schritt eins und zwei von
oben), dann den Aufruf. Er hängt am Dokument statt an der
Dokumentensammlung, und der objectKey ist der der neu
bereitgestellten Datei – ein bereits importierter lässt sich nicht ein
zweites Mal verwenden.
NEW_OBJECT_KEY="kanzlei-mustermann/staging/4b7e.../Schriftsatz.pdf" # objectKey der neuen Datei
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"Auch das antwortet mit 202; die Vorgangsabfrage nennt
anschließend die neue Version:
{
"status": "succeeded",
"result": {
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"version": 4,
"importedBytes": 187001
}
}Akte, Register, Titel und Dokumentklasse bleiben unverändert – es
ändert sich nur der Inhalt. Das comment beschreibt die neue
Version; anders als beim Import eines neuen Dokuments wird daraus
nicht der Titel des Dokuments, und ohne
comment wird der Kommentar der bisherigen Version
übernommen. Standardmäßig übernimmt das Dokument den neuen Dateinamen;
mit "keepFileName": true behält es den bisherigen. Der
fileName muss – wie beim Import – eine Endung tragen, sonst
antwortet der Aufruf mit 400. Ein unbekanntes Dokument
führt zu failed mit
errorCode: "NOT_FOUND".
Ein Dokument, das gerade jemand anderes bearbeitet, wird nicht überschrieben. Ist es in Bearbeitung, zwischenzeitlich geändert, eingefroren oder zum Löschen vorgemerkt, endet der Vorgang mit
errorCode: "CONFLICT"– dann das Dokument erneut lesen und den Aufruf wiederholen.
Ein Dokument umlegen
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"Diese Antwort kommt synchron:
{
"documentId": "b41e77a0-2c33-4f9e-b0a1-5d70e2f6ac18",
"previousContainerId": "1f0c9d1e-6f3b-4a2c-9d20-6b0f4f8a1c77",
"previousTrayId": 17,
"currentContainerId": "3b21...",
"currentTrayId": 4
}Zielakte und Zielregister werden vorab geprüft. Gibt es die Akte
nicht, oder hat sie das angegebene Register nicht, ist die Antwort
404 NOT_FOUND und die Meldung benennt, was fehlt – nicht
etwa ein Serverfehler, der zum Wiederholen einlädt.
targetTrayId darf 0 sein: das legt das
Dokument direkt in die Akte, in kein Register.
Dasselbe in C
Ein minimaler Client, der eine Akte sucht und ein Dokument importiert. Das Zugriffstoken stammt aus der Anmeldung und wird hier als bereits vorhanden angenommen.
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) Akte suchen
var search = await http.GetFromJsonAsync<SearchResponse>(
"v1/dms/documents/container?searchTerm=Mustermann&pageSize=25");
var container = search!.Containers[0];
// 2) Ablageplatz anfordern
var slot = await (await http.PostAsJsonAsync(
"v1/platform/uploads/single", new { fileName = "Schriftsatz.pdf" }))
.Content.ReadFromJsonAsync<UploadSlot>();
// 3) Datei direkt in die Ablage schreiben - ohne Token, die Adresse ist signiert
using (var raw = new HttpClient())
using (var content = new StreamContent(File.OpenRead("Schriftsatz.pdf")))
{
(await raw.PutAsync(slot!.PutUrl, content)).EnsureSuccessStatusCode();
}
// 4) Import beauftragen - derselbe Schlüssel bei jedem Wiederholungsversuch
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) Ergebnis abholen - mit Abbruch, sonst läuft die Schleife bei einer
// Störung endlos weiter
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}");Die vier Datenklassen dazu:
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 bindet die Namen ohne weiteres Zutun, weil
die Schnittstelle in camelCase antwortet und der Standardvergleich Groß-
und Kleinschreibung ignoriert.
In einer produktiven Integration kommen zwei Dinge hinzu, die dieses Beispiel der Kürze halber auslässt:
-
Wiederholungen mit Wartezeitverlängerung bei den
Antwortcodes
429,502,503und504– siehe Idempotenz und Wiederholungen. -
Auswertung des Fehlerumschlags, um
codeundsupportCodemitzuprotokollieren – siehe Fehlercodes.
Ein Wiederholungsversuch liefert nicht das ursprüngliche Ergebnis. Wird derselbe
Idempotency-Keyerneut geschickt, antwortet die Schnittstelle mit409 IDEMPOTENCY_KEY_REUSEDbzw.504 IDEMPOTENCY_KEY_INFLIGHT_TIMEOUT– der Auftrag wird also kein zweites Mal ausgeführt, aber sein Ausgang wird auch nicht nachgeliefert. Den muss die Anwendung über die Vorgangsabfrage klären.
Weiterführend
Verknüpfung mit