Skip to main content

Handling 409 Conflicts

What happens when POST /clients returns 409 — and how to handle each scenario.

This guide assumes you've read Client Onboarding.

Overview

A 409 Conflict means the submitted user already exists in Debitura. The type field tells you which scenario you're in:

TypeWhat it meansWhat to do
ClientExistsNeedsLinkingUser has a Debitura account, not linked to youPresent the approval URL — user decides what to do
ClientAlreadyLinkedToAnotherPartnerUser is linked to a different referral partnerHard block — contact Debitura support

These are the only two conflict types the API returns. A matched account that is not a creditor surfaces differently — see InvalidClientType below.

How Debitura detects existing clients

Debitura matches the submitted email addresses against existing accounts using exact-email matching — domain-suffix matching is intentionally disabled for this endpoint. If a match is found and the user isn't already linked to your partnership, you get a 409 with an approval URL.

ClientExistsNeedsLinking

{
"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=abc123..."
},
"isAttributedClient": false
}
}

Present onboardingLinks.url to the user — as with all referral partner flows, Debitura does not send emails, so you are responsible for delivering this URL.

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.

You receive a client.link_requested webhook immediately when the 409 fires — this confirms the approval URL has been issued. If the user doesn't act, you receive a client.link_expired webhook when the link request expires.

No information about the matched account is returned

For privacy, the 409 response does not reveal any details about the user's existing Debitura account. You only receive back your own externalTenantId and the approval URL.

The response body includes client (an empty object) and users (an empty array) for JSON-shape stability — do not rely on either being populated. Emails of the matched account are never returned.

What the user sees

When the user follows the approval URL, they log in and see a choice page with up to two options:

"Connect to existing account" — shown when the user manages a Debitura creditor that is not already linked to your partnership. The user can select which existing account to link.

"Set up a new account" — always available. Creates a fresh creditor from the company details you submitted in the original request.

If all of the user's existing accounts are already linked to you (the multi-entity scenario), only "Set up a new account" is shown.

Path A: User connects an existing account

The user selects one of their existing creditor accounts to link to your partnership.

  1. A ReferralPartnerClientLink is created for the existing creditor (the link points at the creditor the user picked — not a brand-new one)
  2. You receive a client.linked webhook
  3. isAttributedClient is false — the user existed in Debitura before your referral. See Attribution for how this affects revenue.

Cases from your original POST /clients payload are created automatically on this path immediately after the user approves the link. AllowPendingContracts=true is forced on replay regardless of what you sent on the original request — so if the SDCA, PoA, or KYC are not yet signed at the time of approval, cases enter the Pending contract signing lifecycle state instead of failing with a 422. They advance automatically once the client signs. You receive case.created webhooks for each successful case; there is no case.failed webhook on this path — detect failures via the case lifecycle field. Other case creation failures (e.g. duplicate creditorReference) are reported per-case but do not block the link.

After approval, the user is directed to the signing flow to complete any outstanding contracts.

Concurrent approval requests

If two approval requests for the same client arrive simultaneously, only one succeeds. The second returns a 409 indicating the link request has already been processed. This is expected — you will receive a single client.linked webhook.

Path B: User creates a new account

The user creates a fresh creditor account from the company details you originally submitted.

  1. A new creditor is created from the stored company details
  2. The user is added as admin and redirected to sign the SDCA
  3. You receive a client.linked webhook
  4. Any cases included in your original POST /clients payload are replayed against the new creditor from the stored request (same validation pipeline as first-time creation). AllowPendingContracts=true is forced on replay regardless of what you sent on the original request — so if the SDCA, PoA, or KYC are not yet signed at replay time, cases enter the Pending contract signing lifecycle state instead of failing with a 422. They automatically advance once the client signs. You receive case.created webhooks for each successful case; there is no case.failed webhook on this path — detect failures via the case lifecycle field. Cases whose creditorReference already exists are reported as failures — no duplicates. Case creation failures do not block the account creation; the link is already persisted.
  5. isAttributedClient is false — because the user already existed in Debitura, even though the creditor account is new

POST /clients is an onboarding endpoint only. Once a link exists for the externalTenantId, re-calling POST /clients does not create or retry cases. Every case in the request is reported as a failure with errorType: "IdempotencyViolation" (message "Client already exists; cases not created on repeated call") and the case-creation pipeline is never run — nothing is created.

The cases included in your original onboarding request are still created automatically when the link is established (see Path A and Path B above). But to add new cases or retry failed ones after the link exists, switch to the bearer-token Customer API flow documented in case submission. Do not re-call POST /clients to submit cases — it will silently report every case as an IdempotencyViolation and create none.

A repeat call returns 201 or 202 (depending on SDCA status) with all submitted cases listed under caseResults.failedCases:

{
"externalTenantId": "your-tenant-id",
"caseResults": {
"successfulCases": [],
"failedCases": [
{
"caseIndex": 0,
"creditorReference": "INV-001",
"errorType": "IdempotencyViolation",
"errorMessage": "Client already exists; cases not created on repeated call"
}
]
}
}

Multi-entity: same user, multiple companies

A common real-world scenario: your user manages Company A (already onboarded through your platform) and now wants to add Company B.

Calling POST /clients with a new externalTenantId but the same user email returns 409 ClientExistsNeedsLinking. Since Company A is already linked to you, the choice page only shows "Set up a new account" for Company B.

After the user creates the new account, they can switch between their companies in the Debitura portal. Both companies are linked to your partnership, each with its own externalTenantId:

CompanyexternalTenantIdisAttributedClient
Company A (first onboarded)tenant-atrue
Company B (added later via 409 flow)tenant-bfalse

Each company has independent cases, contracts, and revenue tracking. Use the externalTenantId to distinguish them in your system.

ClientAlreadyLinkedToAnotherPartner

The user's Debitura account is already linked to a different referral partner. Only one partner link per creditor is allowed.

{
"type": "ClientAlreadyLinkedToAnotherPartner",
"message": "This client is already linked to another referral partner. Only one referral partner link per client is currently supported. Please contact Debitura support at contact@debitura.com for assistance.",
"data": {
"externalTenantId": "your-tenant-id"
}
}

This is a hard block. Contact partnerships@debitura.com to discuss resolution.

The data object echoes your externalTenantId for request correlation only — no information about the other partner or the matched creditor is returned.

InvalidClientType

InvalidClientType is defined in the conflict-type enum but is not currently returned by POST /clients. If the matched Debitura account is not a creditor — for example, a collection partner account — the request fails with a 500 Internal Server Error (no type discriminator), not a 409. This is a data-integrity case that should not occur in practice. If you hit it, contact partnerships@debitura.com.

Attribution for 409 flows

All 409 resolution paths set isAttributedClient = false — meaning you earn referral fees only on cases submitted through your API, not on cases the client creates directly. The flag and the fee percentage are locked at link creation time and never change.

Business context: See Referral Program Overview for the full revenue model. For the technical details of attributed vs non-attributed, see Attribution.