Schnellstart mit curl und CSharp

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 und 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.

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": "Klageschrift.pdf",
      "documentType": "pdf",
      "sizeBytes": 184320,
      "modifiedAt": "2026-08-19T14:02:11Z"
    }
  ],
  "totalCount": 1,
  "nextContinuationToken": null
}

Die Register einer Akte – ihre Untergliederung, in die Dokumente einsortiert werden – und deren Inhalt lassen sich analog abrufen:

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"

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"              # objectKey

Der 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"
}

202 heiß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 dem 202 aufhört, meldet Erfolge, die keine sind.

Die operationId ist 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": "Der Benutzer besitzt keine Schreibrechte auf dieser Akte.",
  "errorCode": "ON_PREM_PERMISSION_DENIED"
}

Der Abrufzeitraum beträgt sieben Tage. Danach antwortet die Vorgangs-Adresse mit 404. Ein failed mit errorCode: "OPERATION_TIMEOUT" bedeutet, dass die Verarbeitungsfrist abgelaufen ist, ohne dass ein Ergebnis eingetroffen wäre.

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=...",
    "sizeBytes": 184320,
    "fileName": "Schriftsatz.pdf",
    "version": 3
  }
}
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. Sie sollte nicht zwischengespeichert werden – stattdessen die Vorgangsabfrage erneut aufrufen. Eine bestimmte Fassung eines Dokuments lässt sich über ?version=2 anfordern.

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
}

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, 503 und 504 – siehe Idempotenz und Wiederholungen.
  • Auswertung des Fehlerumschlags, um code und supportCode mitzuprotokollieren – siehe Fehlercodes.

Ein Wiederholungsversuch liefert nicht das ursprüngliche Ergebnis. Wird derselbe Idempotency-Key erneut geschickt, antwortet die Schnittstelle mit 409 IDEMPOTENCY_KEY_REUSED bzw. 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