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

# Errors and safe retries

> Handle typed paywise errors, idempotency outcomes, rate limits, and transient failures without duplicating effects.

Errors are English-only and machine-readable on all three APIs. Branch on
`code` and nested error codes; reserve `detail` and `message` for logs and
developer-facing output. Nested `field` values are JSON paths into the request
body (`claims[0].principal_amount`, `debtor_id`,
`additional_debtor_ids[0]`) or, for query strings, the parameter name.

```json theme={null}
{
  "detail": "The request contains invalid fields.",
  "code": "validation_error",
  "errors": [
    {
      "field": "claims[0].principal_amount",
      "code": "unknown_field",
      "message": "Unknown field."
    }
  ]
}
```

## Retry writes safely

Every effectful `POST` requires `Idempotency-Key`. Generate one opaque key per
logical command and retain it until the outcome is known.

```http theme={null}
POST /v2/orders/20000000-0000-4000-8000-000000000001/finalize/ HTTP/1.1
Host: api.paywise.de
Authorization: Bearer <api-key>
Idempotency-Key: 80000000-0000-4000-8000-000000000001

HTTP/1.1 409 Conflict
Content-Type: application/json
X-Paywise-Request-Id: 90000000-0000-4000-8000-000000000001
X-Paywise-Environment: production

{
  "detail": "The resource changed state and cannot accept this command.",
  "code": "conflict"
}
```

The finalize operation accepts an empty body or exactly `{}`. A successful call returns the
complete `Order` representation defined by the operation reference; the fully
valid error above shows a lifecycle conflict that must be resolved rather than
blindly retried.

The key is scoped by tenant, method, operation, and normalized path. For Case
Management commands the tenant is the selected company; for Partner management
commands it is the Partner owner. Direct and Partner credentials authorized for
the same Case company share its retry namespace. The path is compared
case-insensitively, so an upper-case resource id names the same command as its
lower-case form. Equivalent JSON object-key order and multipart part order
replay; array order remains significant.

Rotating an API secret or replacing a token preserves retry protection. Retry
with the same `Idempotency-Key` and request after changing credentials; the
replacement credential must still have the operation's required scopes and
current access to the tenant. A changed request returns
`409 idempotency_key_conflict`. Use different keys for different logical
commands, including when several integrations act for the same company.

* An exact completed retry replays the original status, body, and application
  headers and adds `Idempotency-Replayed: true`.
* The same key with a different request returns typed `409`.
* A retry while the original call is running waits up to 2 seconds for it. If
  the original finishes in that time, the retry receives its response with
  `Idempotency-Replayed: true`; otherwise it returns
  `409 idempotency_request_in_progress` with `Retry-After: 2`.
* A completed response replays for 24 hours. After that the key answers
  `409 idempotency_key_expired` for another 24 hours and is then forgotten
  (48 hours in total): a request with it executes as a new command. When a
  Partner's access to a company ends, its stored responses for that company
  expire at once: they answer `409 idempotency_key_expired` for 24 hours and
  are then forgotten.
* A `5xx` outcome is never stored: a failure raised inside the command —
  including `503 service_unavailable` from a transient database or storage
  fault — releases the key, so retrying the same key re-executes the command
  instead of replaying the failure. Only an error status the command itself
  returns as its result claims the key. The same release applies to a raised
  `4xx`: a corrected retry under the same key executes normally.
* The header holds exactly one key; a value containing `,` (typically two
  `Idempotency-Key` headers folded together) is `400 validation_error`.

Domain writes, webhook deliveries, and the stored response commit together;
paywise starts its internal follow-up work after that commit, and a replayed
request never repeats an effect. After a server-side failure the same key can
be retried at once. After a network timeout, retry with the same key—never
generate another key for the same logical command.

## Recover by status

| Status | Recovery |
| - | - |
| `400` | Correct validation, query-parameter, or company-context input before a new request. A malformed body (invalid JSON, nesting deeper than 64 levels, non-finite numbers, an unpaired `\uD800`-style surrogate escape, or a duplicate object member) is `parse_error`. A request path containing a NUL character is `validation_error` with `errors[].field = "path"` and `null_characters_not_allowed`. |
| `401` | Fix the credential or environment; do not blind-retry. `authentication_failed` names a missing, unknown, or disabled credential. On `/v2/` and `/partner/v2/`, wrong-host API keys are classified by prefix or shape before credential lookup (`pw_sbx_…` on production, or a 40-hex production key on sandbox), so `environment_mismatch` does not confirm that the key exists or is enabled. |
| `403` | Check the selected company and the error code; use the API surface the code names. Contact paywise support if access is unexpectedly denied. The `403` vocabulary is closed: `permission_denied` (the credential is not permitted for the operation; on the Mahnservice API also a key not bound to a company), `use_partner_webhooks`, `terms_acceptance_required`, `subscription_required` and `company_locked` (Mahnservice API), `sandbox_feature_unavailable`, `sandbox_source_managed`, and `sandbox_reset_requires_admin` (sandbox). |
| `404` | Verify the resource ID and selected tenant. Unknown paths and malformed IDs use the generic neutral detail `The requested resource was not found.` Tenant-scoped resource lookups are also neutral but may use either that generic wording or a resource-worded detail, such as `No mandate found for the given reference.` or `No enforceable title found for the given reference.`, without revealing cross-tenant existence. Unknown paths and malformed IDs answer the same JSON `404` on every method, in the sandbox as well. Slashless Case Management and Partner roots and routes also return JSON `404` for every method, including `GET`, `HEAD`, `OPTIONS`, and writes, with no redirect or `Location` header. See [canonical URLs](/api-docs/essentials/requests-and-responses#canonical-urls) for the separate Mahnservice behavior. The schema endpoints (`/v2/schema/`, `/partner/v2/schema/`, `/mahnservice/v1/schema/`) answer their `404`, `406`, and `429` with the same envelope. |
| `405` | The method is not offered on this route. On routes that have `PATCH`, a `PUT` answers `Use PATCH for partial updates.` |
| `406` | Your `Accept` header excludes every representation the route serves. Downloads serve `*/*`, `application/octet-stream`, `application/pdf`, `image/*`, and `application/json`; `text/html` is `not_acceptable`. |
| `409` | Inspect the typed lifecycle or idempotency code. A redelivery of a webhook delivery that is still `pending`, `retrying`, or `delivering` is `delivery_in_progress`; wait for it to settle. |
| `411` | Send a `Content-Length`; a chunked request body without one is `length_required` and is never executed. |
| `412` | Your `If-Match` validator is stale; re-read the resource and retry with its current `ETag`. |
| `413` | The declared `Content-Length` exceeds the [body ceiling](/api-docs/essentials/limits); split the command. |
| `415` | Use the operation's supported media type (JSON everywhere; multipart only on document uploads) with a Unicode charset. A `Content-Type` charset other than UTF-8, UTF-16, or UTF-32 is `unsupported_media_type` with `Unsupported charset; use UTF-8.` |
| `422` | The draft is not ready for the requested transition (`finalize`); resolve every entry in `errors[]` and retry. |
| `429` | Wait at least the integer seconds in `Retry-After`. |
| `503` | Retry with backoff after `Retry-After` (`service_unavailable`); a required live service — the credential store during authentication, the database during a command, or object storage during a download or a document upload — is unavailable. On an idempotent command the key is released, so the retry executes normally. |
| `500` | Retry transient failures with backoff and the same idempotency key. |

## Common error codes

The top-level `code` names the failure class; nested `errors[].code` names the
rule a field broke. Codes you should branch on beyond `required`,
`invalid`, `blank`, `max_length`, `invalid_choice`, and `unknown_field`:

| Code | Where | Meaning |
| - | - | - |
| `invalid_parameter` | query string | `limit` or `offset` (ASCII digits only), a boolean filter (`archived`, `answered`, `enabled`), a timestamp filter (`updated_since`, `created_after`, `created_before` — timezone-aware RFC 3339 only), `acting_as` (`consumer` or `business`), or an `ordering` term is not a valid value. |
| `invalid_company_context` | `X-On-Behalf-Of-Company` | The header is not the canonical hyphenated UUID (braces, `urn:uuid:`, and 32-hex forms are rejected). |
| `environment_mismatch` (`401`) | credential | On `/v2/` and `/partner/v2/`, a key classified by prefix or shape as belonging to the other environment is rejected before credential lookup. |
| `unsupported_media_type` (`415`), `length_required` (`411`) | request framing | Non-Unicode charset; chunked body without `Content-Length`. |
| `service_unavailable` (`503`) | database, object storage, usage store | Transient fault; retry after `Retry-After`. |
| `null_characters_not_allowed` | request path | The path contains a NUL character. |
| `delivery_in_progress` (`409`) | `redeliver` (Case and Partner) | The delivery is still `pending`, `retrying`, or `delivering`. |
| `claim_not_editable` (`409`) | claim `PATCH` or `DELETE`; `POST /v2/claims/` with a non-draft `order_id` | The claim's current order is no longer a writable draft. Refetch the stable claim URL and continue from its effective status; do not create a replacement. On create, the `order_id` names an order that is no longer a draft: for a finalized order the `detail` reads `The order is no longer a draft; claims can only be added to draft orders.`; a merged order answers the same code with `Order was merged into another order.` Branch on the code and add the claim to a draft order instead. |
| `order_expired` (`409`) | every write on an expired draft order and below it — order `PATCH` and `DELETE`, `finalize`, `withdraw`, `POST /v2/claims/` naming it, claim, document, payment, enforceable-title, rental-agreement, and message commands | The draft passed its `expires_at` (`The draft order has expired and can no longer be changed.`); nothing refreshes it, and the daily cleanup deletes it. Create a new order. |
| `claim_moved_retry` (`409`) | claim, title, document, or payment command | Paywise review moved the claim while the command was acquiring a stable parent. Retry the same logical command against the same `/v2/claims/{claim_id}/...` URL and reuse its idempotency key when the operation is a `POST`. |
| `duplicate_part` | multipart uploads | A part (`type`, `filename`, `base64`, `file`) was sent twice. |
| `not_allowed` | request-to-client `documents` | The question type takes no documents (only file-upload and free-text types do). |
| `duplicate` | `additional_debtor_ids`, nested `legal_representatives[i].legal_representatives[j]` | The same person or organization is attached twice. |
| `delay_before_due` (`422` readiness) | `claims.{id}.delay_date` | `delay_date` must be after `due_date`. |
| `unknown_parameter` | query string | The route does not accept this query parameter, including the retired `sort` and `direction` on `GET /v2/orders/` and `GET /v2/mandates/`, and `ordering` on a Mahnservice detail or sub-resource route. |
| `unexpected_body` | body-less commands (`rotate-secret`, `redeliver`, Partner membership `resend` and `cancel`, Mahnservice actions) and every Case Management and Partner `DELETE` sent with a body | The command takes no request body; `{}`, `[]`, and `null` count as a body, and nothing is deleted. |
| `control_characters` | any text field | A single-line field contains a control character (newline, tab, NUL, U+2028/U+2029, …); a multi-line field (`items[].description`, `legal_basis.description`, `dispute_reason`, message `body`, withdrawal `reason`, request-to-client answers) contains a control character other than TAB, LF, or CR. Format characters are stripped, line breaks are stored as `\n`, and the text is NFC-normalized instead. |
| `total_too_large` | money fields | The amount exceeds the representable total. |
| `negative_total` | `claims.{id}.total_amount`, `totals.order_value` | Charges and payments would leave a negative balance. |
| `items_total_mismatch` | `items` | The item lines do not sum to `principal_amount` (± 0.01). |
| `overpayment` | payments, `principal_amount`, `additional_charges` | A payment on a draft claim exceeds its open value, or an edit would push existing payments above the new value. |
| `duplicate_payment`, `duplicate_payment_report` | payments | Same payment reported twice — see [Payments](/api-docs/case-management-api/concepts/payments). |
| `duplicate_document_reference` | `claims[i].document_reference` | Two claims of one order share a document reference. |
| `invalid_for_legal_form`, `legal_guardian_required`, `minor_debtor`, `minor` | debtors and legal representatives | Check the party's legal form, age, and representative relationships — see [Debtor validation rules](/api-docs/case-management-api/concepts/debtors#debtor-validation-rules). |
| `own_domain_email`, `invalid_email`, `invalid_phone`, `invalid_iban`, `invalid_bic`, `bic_iban_mismatch`, `zip_country_mismatch` | debtor contact and bank data | Format rules on channels, bank accounts, and addresses. |
| `invalid_port`, `fragment_not_allowed`, `invalid_host` | webhook `url` | The endpoint URL is not a plain, publicly reachable HTTPS origin. |
| `use_partner_webhooks` (`403`) | Case webhook writes with `X-On-Behalf-Of-Company` | Partner principals manage endpoints through `/partner/v2/webhooks/`. |
| `company_offboarded`, `membership_not_pending`, `email_not_invitable` | Partner API | Membership lifecycle conflicts: `company_offboarded` on every membership command of an offboarded company; a released company answers `404` on every company and membership request — see [Manage memberships](/api-docs/partner-api/workflows/manage-memberships). |
| `subscription_required`, `company_locked` (`403`) | Mahnservice API writes | No active Mahnservice subscription, or the company is administratively locked — see [Ownership and access](/api-docs/mahnservice-api/concepts/ownership-and-access). |
| `invoice_paid`, `invoice_cancelled`, `invoice_written_off`, `invoice_archived`, `invoice_in_inkasso`, `invoice_settled`, `not_api_invoice`, `company_paused` (`409`) | Mahnservice API | Invoice or dunning lifecycle conflicts — see the [invoice lifecycle precedence table](/api-docs/mahnservice-api/concepts/invoice-lifecycle). |
| `encrypted`, `page_limit_exceeded`, `corrupt`, `unsupported` | document `file` / `base64` | The upload was rejected synchronously; the same values appear as `failure_reason` when a document fails later. |
| `parse_error` | request body | Invalid JSON, nesting deeper than 64, non-finite numbers, unpaired surrogate escapes, or duplicate object members. |

Record the fresh `X-Paywise-Request-Id` when contacting support. Caller-supplied
request IDs are ignored. Validation failures and idempotent replays consume
rate-limit capacity; authentication, tenant-context, and access failures do not.

## Inspect rate-limit headroom

`GET /v2/usage/` and `GET /partner/v2/usage/` return the live buckets used for
enforcement. Usage and info are ordinary reads, consume principal capacity,
and can return `429`; usage includes its own request in the used count.
Direct Case and Mahnservice requests charge credential and company buckets;
Partner management charges credential and Partner; Partner-on-behalf Case
requests charge credential, Partner, and selected company. Each allows 600
requests per minute. Reads have no per-operation bucket; every write also
charges `endpoint:<basename>.<action>` at 120 per minute.
Webhook test delivery and manual redelivery share a company-scoped
20-per-hour expensive-operation bucket. Expensive buckets are checked before
the operation and charged only after a qualifying success (`200` download,
`202` accepted dispatch); other outcomes do not charge. They do not reserve
capacity for in-flight work, so concurrent successes can all pass the
pre-check. Once the stored bucket is full, later requests answer `429` up
front.

There are no `X-RateLimit-*` headers. If the shared counter store is
temporarily unavailable, normal enforcement fails open and emits an
operational signal; the usage operation fails closed with typed `503` instead
of returning fabricated counters.

See [finalize an order](/api-docs/case-management-api/reference/orders/finalize-order),
[Case usage](/api-docs/case-management-api/reference/usage/get-rate-limit-headroom), and
[Partner usage](/api-docs/partner-api/reference/usage/get-rate-limit-headroom) for the current
operation contracts.


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