Source: https://docs.debitura.com/referral-partners/error-reference

# Error Reference

Every error a referral partner integration can encounter, organized by HTTP status code.

For the standard error response format and retry strategy, see [Error Handling](https://docs.debitura.com/developer-concepts/error-handling.md). For symptom-based debugging, see [Troubleshooting](https://docs.debitura.com/referral-partners/troubleshooting.md).

<a id="collection-requests-starter-integration"></a>

## Collection requests (Starter Integration)

`POST /collection-requests` reports most outcomes in the response body, not as errors. Branch on `status`; `201`, `202` and `422` are hints only.

| Response | What it means | What to do |
| --- | --- | --- |
| `400` | The request is unreadable: malformed JSON, wrong types, missing `externalTenantId` or `creditorReference`, more than one invoice, or size limits | Fix the request; nothing was stored |
| `422` with `status: rejected` | Debitura can't take this invoice, e.g. the debtor's country isn't covered or a valid amount is below the minimum | Show the returned reason; there is no link |
| `202` with `status: needs_fixing` | The invoice is readable but has correctable errors; it is now a draft the client owns | Open `solutionUrl`; saving alone creates no case |
| `status: expired` | An approval for an existing client expired or was declined | A new request asks again |
| Same `Idempotency-Key`, different payload | Refused within the 24-hour window | Use a new key for a new submission |
| Timeout or `5xx` | A dependency failed | Retry with the same `Idempotency-Key`, or read the request before resubmitting |

The rest of this page covers the Full Integration endpoints (`POST /clients`, `POST /oauth/token`, the Customer API), which also apply when a Starter client uses them. See [Build the collection button](https://docs.debitura.com/referral-partners/integration-guides/starter-integration.md#4-handle-failures).

<a id="400-bad-request"></a>

## 400 Bad Request

Validation failures. The request body or query parameters are invalid. Fix the input and retry.

**Response format:** Varies by endpoint. Field-validation errors are typically a JSON object keyed by field name with arrays of error strings. Value-level business validations return the structured `ApiErrorResponseDto` shape (`{ error, message, businessErrors: [] }`). Malformed JSON or type-mismatch errors from model binding return [ASP.NET ProblemDetails](https://datatracker.ietf.org/doc/html/rfc7807).

```json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Users": ["At least one user is required"]
  },
  "traceId": "..."
}
```

<a id="client-onboarding-post-clients"></a>

### Client Onboarding (`POST /clients`)

| Error | Cause |
| --- | --- |
| RFC 7807 validation response | Missing, empty, malformed, or type-invalid request body; `[ApiController]` handles this before the action runs |
| `At least one user is required` | `users` array is empty (a null `users` array returns `The Users field is required.`) |
| `When multiple users provided, exactly one must have IsOnboardingUser=true` | Multiple users, none marked as onboarding user |
| `Only one user can be marked as IsOnboardingUser` | Multiple users have `isOnboardingUser: true` |
| `Unknown country: XX` | `client.country` is not a valid ISO 3166-1 alpha-2 code or country name |
| Field-keyed object with `Cases[n].Field` keys (e.g. `{"Cases[0].DueDate": ["..."]}`) | Cases included in the request have invalid fields; client creation is aborted. Checked before the client is created: `creditorReference` repeated within the request, claim lines and age buckets, debtor country, debtor `type`, and debtor address and city. Annotation-level case errors arrive inside the Validation Problem Details `errors` object instead. |

<a id="case-submission-post-cases"></a>

### Case Submission (`POST /cases`)

| Error | Cause |
| --- | --- |
| `Invalid currency code: XYZ` | `currencyCode` not recognized |
| `Case date cannot be in the future` | `date` is a future date |
| `Due date cannot be earlier than the case date` | `dueDate` is before `date` |
| `Due date must be in the past` | `dueDate` is strictly in the future (tomorrow or later) — today is accepted |
| `Case total must be a positive amount` | `amountToRecover` is zero or negative |
| Validation Problem Details `errors.Debtor`: `The Debtor field is required.` | `debtor` is explicitly `null` |
| `Invalid debtor type – allowed values are 'Company' or 'Private' (case-insensitive)` | `debtor.type` is not `"Company"` or `"Private"` (case-insensitive) |
| `Invalid country code: ZZ` | `debtor.countryAlpha2` is not a valid ISO alpha-2 code. If using `debtor.country` (name), the error message will suggest using `countryAlpha2` instead. |
| `A state code ... is required for US debtors` | US debtor without `debtor.stateAlpha2` or `debtor.state` |

<a id="webhooks"></a>

### Webhooks

| Error | Cause |
| --- | --- |
| Model state errors | Missing `url` or `events` on `POST /v1/webhooks` |
| URL validation error | Webhook URL is HTTP, localhost, private IP, or uses a non-standard port (only 443/8443 allowed) |
| `'<type>' is not a valid referral partner event type. Valid types: ...` | Unknown event type on `POST /v1/webhooks/test`. Valid types: `case.created`, `case.updated`, `case.closed`, `client.onboarding.poa_signed`, `client.onboarding.contract_signed`, `client.linked`, `client.link_declined`, `client.link_requested`, `client.link_expired`, `cases.replay_failed` |
| `caseId is required for <type> events.` | Case event test trigger without `caseId` |
| `externalTenantId is required for <type> events.` | Client event test trigger without `externalTenantId` |

The controller-level `POST /v1/webhooks/test` validations above return the standard `ApiErrorResponseDto` / `businessErrors` shape. Data-annotation and model-binding failures are intercepted earlier by `[ApiController]` and return Validation Problem Details instead.

<a id="401-unauthorized"></a>

## 401 Unauthorized

Authentication failed. Check your credentials and environment.

**Response format:** [RFC 7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807).

```json
{
  "title": "Unauthorized",
  "detail": "API Key was not provided",
  "status": 401
}
```

| Detail message | Cause | Fix |
| --- | --- | --- |
| `API Key was not provided` | Missing `XApiKey` header | Add `XApiKey: YOUR_KEY` to every request |
| `Invalid API Key` | Key not found in Debitura | Verify key is correct and matches the environment (test vs production) |
| `Token has expired`, `Token has been revoked`, or a token-validation detail | Token expired (30-min lifetime), revoked, or malformed | Mint a fresh token via `POST /oauth/token`. See the [retry-on-401 pattern](https://docs.debitura.com/referral-partners/getting-started/authentication.md#token-expiry-handling). |

<a id="403-forbidden"></a>

## 403 Forbidden

Test-only endpoints called against production.

```json
{
  "error": "This endpoint is only available in the test environment. Use testreferral-api.debitura.com for testing.",
  "message": "This endpoint is only available in the test environment. Use testreferral-api.debitura.com for testing.",
  "businessErrors": [
    {
      "type": "ValidationError",
      "message": "This endpoint is only available in the test environment. Use testreferral-api.debitura.com for testing.",
      "solutionUrl": ""
    }
  ]
}
```

These endpoints return `ApiErrorResponseDto`, not Problem Details. The reset-client and webhook-test messages include the test host shown above; the deletability check uses the shorter sentence without the host suffix.

Affected endpoints:

-   `DELETE /clients/{externalTenantId}` — [reset test clients](https://docs.debitura.com/referral-partners/getting-started/environments.md#reset-test-clients)
-   `GET /clients/{externalTenantId}/is-deletable`
-   `POST /v1/webhooks/test` — [webhook testing](https://docs.debitura.com/referral-partners/integration-guides/webhook-testing.md)

<a id="404-not-found"></a>

## 404 Not Found

The requested resource does not exist or does not belong to your partner account.

| Endpoint | `ApiErrorResponseDto` type/message | Common cause |
| --- | --- | --- |
| `GET/DELETE /clients/{externalTenantId}` | `NotFound` with an endpoint-specific message | Wrong `externalTenantId`, client archived, or belongs to another partner |
| `POST /oauth/token` | `NotFound`: `No linked client was found for the supplied tenant.` | No active link for this `externalTenantId` — the client was never created, or the link was archived/withdrawn. A token is minted as soon as a non-deleted, non-archived link exists; SDCA does not need to be signed first. |
| `GET /cases/{id}` | `CaseNotFound` with a case-scoped message | Case does not exist or belongs to a client not linked to your partner account |
| `GET/PUT/DELETE /v1/webhooks/{id}` | `WebhookNotFound`: `Webhook not found` | Webhook does not exist or belongs to a different partner — list webhooks via `GET /v1/webhooks` to find valid IDs |

These responses use `error`, `message`, and `businessErrors`; they do not include a Problem Details `title` field.

<a id="409-conflict"></a>

## 409 Conflict

A conflict with existing state. Each conflict type requires different handling.

**Response format:** `type`, `message`, and optional `data` payload.

<a id="clientexistsneedslinking"></a>

### ClientExistsNeedsLinking

The email matches an existing Debitura account. Present `data.onboardingLinks.url` to the user for approval. Approval URLs expire after a configurable per-partner TTL (`ApprovalTtlDays`, default 7 days, clamped 1–30 days). Subscribe to `client.link_expired` to detect expiry; resubmit `POST /clients` with the same `externalTenantId` to generate a fresh approval URL.

Cases submitted in the original request are **persisted server-side** and replayed against the creditor on the resolved client link (the selected existing account or newly created account) automatically once the user approves the link. `AllowPendingContracts=true` is forced on replay regardless of what you sent on the original request, so missing SDCA or PoA can place a case in `PendingContractSigning` rather than returning a contract-related `422`. KYC is evaluated separately and does not itself set that lifecycle on this replay path. There is no `case.failed` event. A replayed case that fails is never created, so it has no `lifecycle`; each failure is reported by a `cases.replay_failed` webhook (`creditorReference`, `failureReason`). Cases whose `creditorReference` already exists for the link are skipped silently. See [Handling 409 Conflicts](https://docs.debitura.com/referral-partners/integration-guides/client-onboarding-conflicts.md) for the full flow.

```json
{
  "type": "ClientExistsNeedsLinking",
  "message": "Client already exists in Debitura. Approval required to link to referral partner.",
  "data": {
    "externalTenantId": "your-tenant-id",
    "onboardingLinks": {
      "url": "https://app.debitura.com/ReferralPartner/Approve?token=..."
    }
  }
}
```

For privacy, the 409 response does not reveal any details about the user's matched Debitura account — `externalTenantId` and `onboardingLinks.url` are the only fields you need to act on. The response also includes `client`, `users`, and `isAttributedClient` for JSON-shape stability with the success response — do not rely on them being populated on this conflict path.

<a id="clientalreadylinkedtoanotherpartner"></a>

### ClientAlreadyLinkedToAnotherPartner

The client has an active link to a different referral partner. Only one partner per creditor. Contact [partnerships@debitura.com](mailto:partnerships@debitura.com).

<a id="usercreationfailed"></a>

### UserCreationFailed

The onboarding user's account could not be created while a new client was being created. Correct the user identity, or contact Debitura support if it persists.

<a id="idempotencykeyinprogress"></a>

### IdempotencyKeyInProgress

`POST /clients` sent with an `Idempotency-Key` while an identical request with the same key is still running. The response carries `businessErrors[0].type: IdempotencyKeyInProgress` (not a top-level `type`) and a `Retry-After: 2` header. Wait and retry with the same key. See [Idempotency](https://docs.debitura.com/developer-concepts/idempotency.md).

<a id="invalidclienttype"></a>

### InvalidClientType

`InvalidClientType` is defined in the conflict-type enum but is **not currently returned**. A non-creditor account (e.g. a collection-partner user) is never matched by client detection. If its email is submitted as a user email, the request fails with `409 UserCreationFailed`; if only the `supportEmail` matches, a new client is created. See [Handling 409 Conflicts](https://docs.debitura.com/referral-partners/integration-guides/client-onboarding-conflicts.md#invalidclienttype).

<a id="approve--decline-race-link-request-already-processed"></a>

### Approve / decline race (link request already processed)

Concurrent attempts to approve or decline the same link request return `409 Conflict` (atomic claim). Only the first caller succeeds; subsequent callers should treat the conflict as "already processed." This typically appears as a user double-clicking the approval button or two browser tabs racing the same approval URL — your integration does not need to retry.

<a id="withdrawal-not-allowed"></a>

### Withdrawal Not Allowed

`POST /clients/{externalTenantId}/withdraw` returns HTTP 409 with `ApiErrorResponseDto` when withdrawal is blocked — either because `isAttributedClient` is false, or cases have progressed beyond `PendingContractSigning`.

```json
{
  "error": "The client cannot be withdrawn. Withdrawal is only allowed for clients your organisation brought to Debitura, and only while all of their cases are still in onboarding.",
  "message": "The client cannot be withdrawn. Withdrawal is only allowed for clients your organisation brought to Debitura, and only while all of their cases are still in onboarding.",
  "businessErrors": [
    {
      "type": "Conflict",
      "message": "The client cannot be withdrawn. Withdrawal is only allowed for clients your organisation brought to Debitura, and only while all of their cases are still in onboarding.",
      "solutionUrl": ""
    }
  ]
}
```

<a id="partner-not-accepting-referrals"></a>

### Partner not accepting referrals

`POST /clients` returns HTTP `409` with top-level `type: "PartnerNotAcceptingReferrals"` when your referral account is paused or closed to new clients. No client, user or link is created by this refusal. Existing clients and their case submissions remain available.

A pre-effect refusal does not consume the `Idempotency-Key`. If a paused account is reactivated, retry the same body and key; do not create a new client identity to bypass the refusal.

<a id="422-unprocessable-entity"></a>

## 422 Unprocessable Entity

Business rule violations. The request is valid but cannot be processed due to domain constraints.

**Response format:** Two shapes are currently reachable. Orchestrator errors use a typed `businessErrors` array with `type`, `message`, and `solutionUrl`. Earlier business-rule checks can instead return a field-keyed dictionary with no `businessErrors` array. Detect the shape before reading it.

```json
{
  "businessErrors": [
    {
      "type": "MissingPowerOfAttorney",
      "message": "Power of attorney not signed with partner X.",
      "solutionUrl": "https://referral.debitura.com/PowerOfAttorney/Sign/..."
    }
  ]
}
```

On `POST /clients`, a `422` stops the whole call before the client is created. It returns `businessErrors[].type: AmountBelowMinimum` when an embedded case is below the minimum claim amount (the message names the case, e.g. `Cases[0]: ...`), or `IdempotencyConflict` when an `Idempotency-Key` is reused with a different request.

On `POST /cases`, the no-partner pre-check uses the legacy shape:

```json
{
  "BusinessRule": ["We don't have an exclusive pre‑legal partner in the provided jurisdiction"]
}
```

| Typed `businessErrors[].type` | Cause | `solutionUrl` | Fix |
| --- | --- | --- | --- |
| `MissingDebtCollectionContract` | SDCA not signed or needs re-signing | Yes | Present URL to client, retry after signing |
| `MissingPowerOfAttorney` | PoA (or [JPA for a jurisdiction pricing zone](https://docs.debitura.com/referral-partners/concepts/jurisdiction-pricing-carveouts.md)) not signed for this jurisdiction | Yes | Present URL to client, retry after signing |
| `MissingKycVerification` | Collection partner requires [KYC](https://docs.debitura.com/referral-partners/integration-guides/kyc-verification.md) and creditor hasn't completed it | Yes | Present URL to client, retry after completion. **Not bypassed by `allowPendingContracts`.** |
| `CreditorBlocked` | Creditor account is blocked | Present as an empty string | Contact Debitura support. Error message includes the reason. |
| `CollectionPartnerNotFound` | `collectionPartnerId` override is unknown or deleted. Message: `Collection partner {GUID} not found`; the legacy `CollectionPartnerId` key carries the same message alongside | No | Omit the override or use a valid collection partner ID |

`NoPartnerAvailable` remains an internal enum value, but `POST /cases` does not emit it in `businessErrors[].type`: the reachable no-match path is the field-keyed `BusinessRule` response shown above.

MissingKycVerification is a hard block

Unlike contract-related errors, `allowPendingContracts: true` does **not** bypass `MissingKycVerification`. KYC is always enforced when submitting via bearer token. See [KYC Verification](https://docs.debitura.com/referral-partners/integration-guides/kyc-verification.md#interaction-with-allowpendingcontracts).

<a id="429-too-many-requests"></a>

## 429 Too Many Requests

Rate limit exceeded. Read `retryAfter` from the response body and wait.

```json
{
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Please try again later or contact support if you need higher limits.",
  "retryAfter": 42.0
}
```

Limits per API key: 2,000 requests/minute, 20,000/hour, 100,000/day. See [Rate Limiting](https://docs.debitura.com/developer-concepts/rate-limiting.md).

<a id="500-internal-server-error"></a>

## 500 Internal Server Error

Transient server error. Retry with exponential backoff (max 3 retries). If persistent, contact [partnerships@debitura.com](mailto:partnerships@debitura.com) with the request details and timestamp.
