> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paywise.de/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage documents

> Upload claim documents, follow asynchronous processing, replace failed or rejected attachments, and download ready files.

## Outcome

The document is attached to the intended parent, has reached `ready`, and can
be downloaded through the authenticated proxy. Failed or rejected attachments
have been recovered without blocking a later review or handoff.

## Prerequisites

* A Bearer key.
* A claim UUID, or the equivalent parent IDs for a message or
  request-to-client answer. Claim document commands never need an order ID.
* A PDF, JPEG, or PNG no larger than 10 MiB.
* A stable idempotency key for each upload command.

## Lifecycle context

Documents are always resource-scoped and scan asynchronously through `pending`,
then `ready`, `rejected`, or `failed`. Only the malware scan and page
processing are asynchronous: the byte sniff, PDF structure checks, size, and
base64 decoding run synchronously and answer `400` without creating a
document. Order finalization does not wait. A failed or rejected attachment
can be deleted and replaced while its parent remains editable; its bytes are
no longer stored, so `download_url` is `null` and a download is `404`. A
ready attachment becomes visible downstream only at the parent workflow's
allowed handoff point.

## 1. Upload to the parent

For multipart upload, let curl set the multipart boundary:

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_API_KEY = "pw_sbx_your_api_key"
  CLAIM_ID = "40000000-0000-4000-8000-000000000001"
  UPLOAD_KEY = "your-upload-key"

  with open("invoice.pdf", "rb") as invoice:
      response = requests.post(
          f"{PAYWISE_API_URL}/v2/claims/{CLAIM_ID}/documents/",
          headers={"Authorization": f"Bearer {PAYWISE_API_KEY}", "Idempotency-Key": UPLOAD_KEY},
          data={"type": "invoice"},
          files={"file": ("invoice.pdf", invoice, "application/pdf")},
          timeout=(5, 30),
      )
  if response.status_code != 201:
      response.raise_for_status()
      raise RuntimeError(f"Expected 201, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  import { openAsBlob } from "node:fs";

  const form = new FormData();
  form.append("type", "invoice");
  form.append("file", await openAsBlob("invoice.pdf", { type: "application/pdf" }), "invoice.pdf");
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/claims/${process.env.CLAIM_ID}/documents/`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}`, "Idempotency-Key": process.env.UPLOAD_KEY },
    body: form,
    signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 201) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.time.Duration;
  import java.util.List;
  import java.util.UUID;

  class UploadDocumentRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String boundary = "paywise-" + UUID.randomUUID();
      byte[] prefix = ("--" + boundary + "\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\ninvoice\r\n"
          + "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"invoice.pdf\"\r\n"
          + "Content-Type: application/pdf\r\n\r\n").getBytes(StandardCharsets.UTF_8);
      byte[] suffix = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8);
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/claims/" + System.getenv("CLAIM_ID") + "/documents/"))
          .timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY"))
          .header("Idempotency-Key", System.getenv("UPLOAD_KEY"))
          .header("Content-Type", "multipart/form-data; boundary=" + boundary)
          .POST(HttpRequest.BodyPublishers.ofByteArrays(List.of(prefix, Files.readAllBytes(Path.of("invoice.pdf")), suffix))).build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 201) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.IO;
  using System.Net.Http;
  using System.Net.Http.Headers;
  using System.Threading;
  using System.Threading.Tasks;

  class UploadDocumentRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var form = new MultipartFormDataContent();
          form.Add(new StringContent("invoice"), "type");
          var file = new StreamContent(File.OpenRead("invoice.pdf"));
          file.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
          form.Add(file, "file", "invoice.pdf");
          using var request = new HttpRequestMessage(HttpMethod.Post, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/claims/{Environment.GetEnvironmentVariable("CLAIM_ID")}/documents/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
          request.Headers.Add("Idempotency-Key", Environment.GetEnvironmentVariable("UPLOAD_KEY"));
          request.Content = form;
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 201) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request POST "$PAYWISE_API_URL/v2/claims/$CLAIM_ID/documents/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" \
    --header "Idempotency-Key: $UPLOAD_KEY" \
    --form 'type=invoice' --form 'file=@invoice.pdf;type=application/pdf' \
    --output document.json --write-out '%{http_code}')"
  [ "$http_status" -eq 201 ] || exit 1
  ```
</CodeGroup>

For a JSON upload, send the `base64` content and `filename` allowed by the
operation. Remote `url` sources are no longer accepted. The stored `filename`
is sanitised (no path components or control characters; the extension follows
the detected content type), so read it back from the response rather than
assuming your input. Use the same idempotency key only when retrying the same
upload after a transport failure.

## 2. Read processing state

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_API_KEY = "pw_sbx_your_api_key"
  CLAIM_ID = "40000000-0000-4000-8000-000000000001"
  DOCUMENT_ID = "60000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/claims/{CLAIM_ID}/documents/{DOCUMENT_ID}/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"}, timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/claims/${process.env.CLAIM_ID}/documents/${process.env.DOCUMENT_ID}/`, {
    method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class ReadDocumentRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String url = System.getenv("PAYWISE_API_URL") + "/v2/claims/" + System.getenv("CLAIM_ID") + "/documents/" + System.getenv("DOCUMENT_ID") + "/";
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 200) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;

  class ReadDocumentRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/claims/{Environment.GetEnvironmentVariable("CLAIM_ID")}/documents/{Environment.GetEnvironmentVariable("DOCUMENT_ID")}/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/v2/claims/$CLAIM_ID/documents/$DOCUMENT_ID/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --output current-document.json --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  ```
</CodeGroup>

Poll this parent-scoped detail endpoint with backoff until `status` is
`ready`, `rejected`, or `failed`. A `failed` document carries a
`failure_reason` (`page_limit_exceeded`, `encrypted`, `corrupt`,
`unsupported`, or `processing_failed`); it is `null` in every other state.
The claim-scoped list also exposes status and timestamps. A titled claim's
title documents use
`/v2/claims/{claim_id}/enforceable-title/documents/…`; they also remain stable
if review moves the claim to another order.
Document-processing webhook events have been retired.

## Representative response

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "60000000-0000-4000-8000-000000000001",
  "type": "invoice",
  "filename": "invoice.pdf",
  "mime_type": "application/pdf",
  "status": "ready",
  "failure_reason": null,
  "parent": {
    "type": "claim",
    "id": "30000000-0000-4000-8000-000000000001"
  },
  "created_at": "2026-08-27T10:10:00Z",
  "updated_at": "2026-08-27T10:10:08Z",
  "download_url": "https://api.paywise.de/v2/claims/30000000-0000-4000-8000-000000000001/documents/60000000-0000-4000-8000-000000000001/download/"
}
```

This is a complete response matching the `DocumentRead` response schema.

## 3. Replace a failed or rejected document

Remove the failed or rejected attachment from an editable parent, then upload
a corrected file with a new idempotency key. There is no document-ingestion
retry endpoint:

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_API_KEY = "pw_sbx_your_api_key"
  CLAIM_ID = "40000000-0000-4000-8000-000000000001"
  DOCUMENT_ID = "60000000-0000-4000-8000-000000000001"

  response = requests.delete(
      f"{PAYWISE_API_URL}/v2/claims/{CLAIM_ID}/documents/{DOCUMENT_ID}/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"}, timeout=(5, 30),
  )
  if response.status_code != 204:
      response.raise_for_status()
      raise RuntimeError(f"Expected 204, received {response.status_code}")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/claims/${process.env.CLAIM_ID}/documents/${process.env.DOCUMENT_ID}/`, {
    method: "DELETE", headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 204) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.time.Duration;

  class DeleteDocumentRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String url = System.getenv("PAYWISE_API_URL") + "/v2/claims/" + System.getenv("CLAIM_ID") + "/documents/" + System.getenv("DOCUMENT_ID") + "/";
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).DELETE().build();
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() != 204) throw new IOException(response.body());
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;

  class DeleteDocumentRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Delete, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/claims/{Environment.GetEnvironmentVariable("CLAIM_ID")}/documents/{Environment.GetEnvironmentVariable("DOCUMENT_ID")}/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, cancellation.Token);
          if ((int)response.StatusCode != 204) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
      }
  }
  ```

  ```bash cURL theme={null}
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request DELETE "$PAYWISE_API_URL/v2/claims/$CLAIM_ID/documents/$DOCUMENT_ID/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --output /dev/null --write-out '%{http_code}')"
  [ "$http_status" -eq 204 ] || exit 1
  ```
</CodeGroup>

A successful delete returns `204` with no response body. Deleting an attachment
whose parent review is closed returns `409`; refetch the parent and escalate
rather than retrying in a loop.

## 4. Download only a ready document

<CodeGroup>
  ```python Python theme={null}
  import requests

  PAYWISE_API_URL = "https://api-sandbox.paywise.de"
  PAYWISE_API_KEY = "pw_sbx_your_api_key"
  CLAIM_ID = "40000000-0000-4000-8000-000000000001"
  DOCUMENT_ID = "60000000-0000-4000-8000-000000000001"

  response = requests.get(
      f"{PAYWISE_API_URL}/v2/claims/{CLAIM_ID}/documents/{DOCUMENT_ID}/download/",
      headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"}, stream=True, timeout=(5, 30),
  )
  if response.status_code != 200:
      response.raise_for_status()
      raise RuntimeError(f"Expected 200, received {response.status_code}")
  with open("downloaded-invoice.pdf", "wb") as output:
      for chunk in response.iter_content(chunk_size=64 * 1024):
          if chunk:
              output.write(chunk)
  ```

  ```javascript JavaScript theme={null}
  import { createWriteStream } from "node:fs";
  import { Readable } from "node:stream";
  const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/claims/${process.env.CLAIM_ID}/documents/${process.env.DOCUMENT_ID}/download/`, {
    method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` }, signal: AbortSignal.timeout(30000),
  });
  if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  await new Promise((resolve, reject) => Readable.fromWeb(response.body).pipe(createWriteStream("downloaded-invoice.pdf")).on("finish", resolve).on("error", reject));
  ```

  ```java Java theme={null}
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.nio.file.AtomicMoveNotSupportedException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.nio.file.StandardCopyOption;
  import java.time.Duration;

  class DownloadDocumentRequest {
    public static void main(String[] args) throws IOException, InterruptedException {
      String url = System.getenv("PAYWISE_API_URL") + "/v2/claims/" + System.getenv("CLAIM_ID") + "/documents/" + System.getenv("DOCUMENT_ID") + "/download/";
      Path destination = Path.of("downloaded-invoice.pdf").toAbsolutePath();
      Path temporary = Files.createTempFile(destination.getParent(), ".paywise-download-", ".tmp");
      HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
      HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
      try {
        HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(temporary));
        if (response.statusCode() != 200) throw new IOException("HTTP " + response.statusCode());
        try {
          Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
        } catch (AtomicMoveNotSupportedException unsupported) {
          Files.move(temporary, destination, StandardCopyOption.REPLACE_EXISTING);
        }
      } finally {
        Files.deleteIfExists(temporary);
      }
    }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.IO;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;

  class DownloadDocumentRequest
  {
      static async Task Main()
      {
          using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
          using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/claims/{Environment.GetEnvironmentVariable("CLAIM_ID")}/documents/{Environment.GetEnvironmentVariable("DOCUMENT_ID")}/download/");
          request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
          using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
          using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellation.Token);
          if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
          await using var output = File.Create("downloaded-invoice.pdf");
          await response.Content.CopyToAsync(output, cancellation.Token);
      }
  }
  ```

  ```bash cURL theme={null}
  temporary_file="$(mktemp ./downloaded-invoice.pdf.XXXXXX)"
  cleanup_download() {
    rm -f -- "$temporary_file"
  }
  trap cleanup_download EXIT HUP INT TERM
  http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
    --request GET "$PAYWISE_API_URL/v2/claims/$CLAIM_ID/documents/$DOCUMENT_ID/download/" \
    --header "Authorization: Bearer $PAYWISE_API_KEY" --output "$temporary_file" --write-out '%{http_code}')"
  [ "$http_status" -eq 200 ] || exit 1
  mv -- "$temporary_file" downloaded-invoice.pdf
  trap - EXIT HUP INT TERM
  ```
</CodeGroup>

Use the documented download route. Do not persist or construct storage URLs.

## Runnable Python workflow

Set `DOCUMENT_FILE` to a local PDF, JPEG, or PNG. The workflow verifies the
sandbox, uploads the file, polls processing state, removes a failed or rejected
attachment while its parent remains editable, and downloads a `ready` document.
After removal, correct the source file and run a fresh upload command.

```python Python workflow theme={null}
import os
import time
import uuid
import requests

PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
CLAIM_ID = "40000000-0000-4000-8000-000000000001"
DOCUMENT_FILE = "./invoice.pdf"
DOCUMENT_OUTPUT = "./downloaded-invoice.pdf"

base_url = PAYWISE_API_URL
if not base_url.startswith("https://"):
    raise ValueError("PAYWISE_API_URL must be an HTTPS sandbox URL")
headers = {"Authorization": f"Bearer {PAYWISE_API_KEY}"}
claim_id = CLAIM_ID
claim_url = f"{base_url}/v2/claims/{claim_id}/"
collection_url = f"{base_url}/v2/claims/{claim_id}/documents/"

claim_response = requests.get(claim_url, headers=headers, timeout=(5, 30))
if claim_response.status_code != 200:
    claim_response.raise_for_status()
    raise RuntimeError(f"Expected 200, received {claim_response.status_code}")
if claim_response.headers.get("X-Paywise-Environment", "").lower() != "sandbox":
    raise RuntimeError("Refusing writes without authenticated sandbox proof")

upload_headers = {**headers, "Idempotency-Key": str(uuid.uuid4())}
document_file = DOCUMENT_FILE
supported_media_types = {
    ".pdf": "application/pdf",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
}
suffix = os.path.splitext(document_file)[1].lower()
media_type = supported_media_types.get(suffix)
if media_type is None:
    raise ValueError("DOCUMENT_FILE must end in .pdf, .jpg, .jpeg, or .png")
with open(document_file, "rb") as source:
    uploaded = requests.post(
        collection_url,
        headers=upload_headers,
        data={"type": "invoice"},
        files={"file": (os.path.basename(document_file), source, media_type)},
        timeout=(5, 30),
    )
if uploaded.status_code != 201:
    uploaded.raise_for_status()
    raise RuntimeError(f"Expected 201, received {uploaded.status_code}")
document = uploaded.json()
document_id = document["id"]
item_url = f"{collection_url}{document_id}/"

for attempt in range(12):
    current = requests.get(item_url, headers=headers, timeout=(5, 30))
    if current.status_code != 200:
        current.raise_for_status()
        raise RuntimeError(f"Expected 200, received {current.status_code}")
    document = current.json()
    if document["status"] != "pending":
        break
    if attempt == 11:
        raise TimeoutError("Document scan did not reach a terminal state")
    time.sleep(min(2 ** attempt, 30))

if document["status"] in {"failed", "rejected"}:
    refreshed_claim = requests.get(claim_url, headers=headers, timeout=(5, 30))
    if refreshed_claim.status_code != 200:
        refreshed_claim.raise_for_status()
        raise RuntimeError(f"Expected 200, received {refreshed_claim.status_code}")
    if refreshed_claim.json().get("status") != "draft":
        raise RuntimeError("Failed or rejected document cannot be deleted from a closed parent")
    deleted = requests.delete(item_url, headers=headers, timeout=(5, 30))
    if deleted.status_code != 204:
        deleted.raise_for_status()
        raise RuntimeError(f"Expected 204, received {deleted.status_code}")
    WORKFLOW_RESULT = {"document_id": document_id, "status": "deleted"}
elif document["status"] == "ready":
    downloaded = requests.get(
        f"{item_url}download/", headers=headers, stream=True, timeout=(5, 30)
    )
    if downloaded.status_code != 200:
        downloaded.raise_for_status()
        raise RuntimeError(f"Expected 200, received {downloaded.status_code}")
    destination = DOCUMENT_OUTPUT
    with open(destination, "wb") as output:
        for chunk in downloaded.iter_content(chunk_size=64 * 1024):
            if chunk:
                output.write(chunk)
    WORKFLOW_RESULT = {"document_id": document_id, "status": "ready", "output": destination}
else:
    raise RuntimeError(f"Unexpected document status: {document['status']}")
```

## Failure and recovery

* `400` or `415`: fix the document type, source mode, size, or media type and
  start a new upload command. Encrypted PDFs (`encrypted`), PDFs above 100
  pages (`page_limit_exceeded`), unparseable bytes (`corrupt`), and other
  content types (`unsupported`) are rejected synchronously on the `file` or
  `base64` field — remove the password or split the file before uploading.
  A malware finding or a scanner that could not run is never a `400`: it
  arrives asynchronously as `rejected` or `failed`.
* `400 duplicate_part`: a multipart body repeated `type`, `filename`,
  `base64`, or `file`; the error names the repeated part. A multipart body
  that is cut off before the file part is complete is reported as a missing
  `file`, so check the upload was sent in full before retrying.
* `503 service_unavailable` on an upload: document storage was briefly
  unavailable and nothing was created. Wait for `Retry-After`, then retry the
  same command with the same idempotency key.
* `404` on `DELETE`: the claim, rental-agreement, or enforceable-title
  document does not exist under that parent. The response is neutral and
  does not reveal whether the ID exists elsewhere.
* `406 not_acceptable` on a download: send `Accept: */*`,
  `application/octet-stream`, `application/pdf`, `image/*`, or
  `application/json`; `text/html` is refused.
* `404 not_found` on a download: the document is not `ready` (including
  `rejected`/`failed`), or its object is missing in storage. `503
  service_unavailable` with `Retry-After: 1` is a storage fault — retry after
  the interval.
* `409` on `DELETE …/enforceable-title/`: the title still owns documents; the
  envelope lists them in `document_ids[]`. Delete those first.
* `409`: the document state or parent edit window changed. Refetch both; delete
  and replace failed or rejected content only when the parent still permits it.
* `429`: wait at least the integer seconds in `Retry-After`, then retry the same
  logical POST with the same idempotency key. Resume GET polling after the wait.
* Timeout, `500`, or `503`: retry a POST with its same key; safe GET and
  download requests may be retried with backoff.

## Verify

```bash theme={null}
jq -e '.status == "ready" and .download_url != null' current-document.json
file downloaded-invoice.pdf
```

A mandate message or an answer to a request to client is handed to paywise
case processing without waiting for scanning, like one submitted in the
paywise portal: its attachments go along as they are, and files already
`failed` or `rejected` are left out. Order review can resume while answer
documents are still processing.

## Related reference

* [POST `/v2/claims/{claim_id}/documents/`](/api-docs/case-management-api/reference/documents/create-claim-document)
* [GET `/v2/claims/{claim_id}/documents/{id}/`](/api-docs/case-management-api/reference/documents/get-claim-document)
* [DELETE `/v2/claims/{claim_id}/documents/{id}/`](/api-docs/case-management-api/reference/documents/delete-claim-document)
* [GET `/v2/claims/{claim_id}/documents/{id}/download/`](/api-docs/case-management-api/reference/documents/download-claim-document)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.