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

# Invoice documents

> Upload the original invoice PDF, wait for asynchronous scanning, and release the invoice only once its document is ready.

Each Mahnservice invoice can carry **one PDF**, up to **10 MiB (10,485,760 bytes)** and **100 pages**. Upload the
bytes as multipart `file` or JSON `base64`, with an optional `filename`.
Remote document URLs are not accepted. Uploading again replaces the current
document, and is allowed only while the invoice is held. After release,
uploads return `409 already_released`; a settled, archived, or
collection-transferred invoice answers `409 invoice_paid`,
`invoice_cancelled`, `invoice_written_off`, `invoice_archived`, or
`invoice_in_inkasso`, a bookkeeping invoice `409 not_api_invoice` — see the
[precedence table](/api-docs/mahnservice-api/concepts/invoice-lifecycle#which-conflict-wins).

The bytes are inspected synchronously before anything is stored. A file that
is not a PDF is `400 unsupported`, a truncated PDF without its `%%EOF` marker
`400 corrupt`, a password-protected PDF `400 encrypted`, and a PDF above 100
pages `400 page_limit_exceeded` — the same codes the Case Management API
uses, reported on `file` or `base64`. A PDF whose markers are intact but
whose body cannot be parsed is accepted at upload, exactly as on the Case
Management API, and processed asynchronously like any other PDF. `filename`
(JSON) and the multipart file name are percent-decoded once and then
validated as a single line: control characters are `400 control_characters`
on `filename` or `file`, invisible format characters are stripped, only the
base name is kept, and a name that decodes to empty, `.`, or `..` is
`400 invalid` (`Provide a valid file name.`); omit `filename` to fall back
to `document.pdf`. If the document store refuses the write, the upload is
`503 service_unavailable` with `Retry-After`; nothing is stored and a
previous document stays in place.

## Upload and keep the document id

```bash cURL theme={null}
curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
  --request POST "$PAYWISE_API_URL/mahnservice/v1/invoices/$INVOICE_ID/documents/" \
  --header "Authorization: Bearer $PAYWISE_API_KEY" \
  --form "file=@invoice.pdf;type=application/pdf"
```

`201 Created` returns the **document**, with its own `id`. The `Location`
response header points to its detail endpoint. Scanning runs asynchronously;
an accepted upload is not yet ready to enclose with a dunning notice.

```json Pending document theme={null}
{
  "id": "40000000-0000-4000-8000-000000000001",
  "filename": "invoice.pdf",
  "mime_type": "application/pdf",
  "size": null,
  "status": "pending",
  "download_url": null,
  "created_at": "2026-09-18T10:00:00+00:00",
  "updated_at": "2026-09-18T10:00:00+00:00"
}
```

## Wait for a terminal scanning result

Poll `GET /mahnservice/v1/invoices/{uuid}/documents/{document_uuid}/` using
the invoice id and the returned document id. You can also read
`GET /mahnservice/v1/invoices/{uuid}/documents/`: it returns an unpaginated
array containing zero or one document.

| `status` | Meaning | Next action |
| - | - | - |
| `pending` | The upload is queued or scanning. | Keep polling with a delay and a deadline. |
| `ready` | Scanning succeeded and the PDF is available. | Release the invoice when your integration is ready. |
| `rejected` | The malware scan rejected the file, or the invoice was settled or archived before the scan finished. | Stop; investigate the file and upload a clean replacement while held. |
| `failed` | Processing could not complete, or the content was invalid or oversized. | Stop; correct the problem and upload again while held. |

`size` and `download_url` are `null` until the document is ready and stay
`null` on a rejected or failed document — its bytes are discarded. On the
invoice, `has_document` becomes `true` only when the current document is
ready; `document` contains its id, status and download URL. A replacement
starts scanning again, so wait for the replacement's result too. A document
whose scan completes after the invoice was paid, cancelled, written off, or
archived lands on `rejected` rather than `ready`: a settled invoice never
gains a document. The invoice's `updated` marker advances when the document
reaches `ready`, `rejected`, or `failed`, not on internal scan retries.

There is no document retry endpoint. To replace a rejected or failed upload,
send the PDF bytes again to the same documents collection. Do not repost a
pending upload merely because a poll has not finished yet.

## Release and download

An invoice can be released without an attachment. **If you have uploaded a
document, it must be ready before release**: `pending`, `rejected` and
`failed` all cause `409 document_not_ready`. A polling deadline expiring means
your integration should stop and check again later; it does not mean the scan
failed.

When ready, `download_url` names the authenticated download endpoint
`GET /mahnservice/v1/invoices/{uuid}/documents/{document_uuid}/download/`.
Send your Bearer credential when following it. Downloading before readiness
returns `404`; an object missing from storage is `404` as well, and a storage
fault is `503 service_unavailable` with `Retry-After` — retry the download.
`HEAD` on the documents collection mirrors `GET` and never uploads.

The [quickstart's complete Python workflow](/api-docs/mahnservice-api/quickstart#6-runnable-end-to-end-workflow)
uploads a local PDF and polls with a bounded wait before releasing.

## Related reference

* [Attach the invoice PDF](/api-docs/mahnservice-api/reference/invoices/attach-the-invoice-pdf)
* [List the invoice documents](/api-docs/mahnservice-api/reference/invoices/list-the-invoice-documents)
* [Get an invoice document](/api-docs/mahnservice-api/reference/invoices/get-an-invoice-document)
* [Download the invoice document](/api-docs/mahnservice-api/reference/invoices/download-the-invoice-document)


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