# payouts — weekly nurse settlement > Client seam `client/src/services/payouts/` · `USE_PAYOUTS_MOCK = true` (**mock is primary**) · 13 server ops > Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29). Money going out, weekly, in batches. The nurse's view is read-only; the admin's view is the one place in the platform where an irreversible transfer is triggered by hand. ## Endpoints | Method | Path | Auth | Verdict | | --- | --- | --- | --- | | GET | `/api/v1/nurse_payouts/earnings` | `[Authorize]` | wired · paginated — per-booking earnings | | GET | `/api/v1/nurse_payouts/earnings_balance` | `[Authorize]` | wired — the four-bucket balance | | GET | `/api/v1/nurse_payouts/history` | `[Authorize]` | wired · paginated | | GET | `/api/v1/nurse_payouts/{id}` | `[Authorize]` | wired — payout detail | | GET | `/api/v1/nurses/{nurseId}/payable_balance` | `[Authorize]` | **unwired** — the nurse reads `earnings_balance` instead | | GET | `/api/v1/admin_payouts/eligible` | admin · `sensitive` | wired · paginated | | GET | `/api/v1/admin_payouts/batches` | admin · `sensitive` | wired · paginated | | POST | `/api/v1/admin_payouts/batches` | admin · `sensitive` | **unwired** — generation is automatic (below) | | GET | `/api/v1/admin_payouts/batches/{id}` | admin · `sensitive` | wired · paginated (**`page`/`pageSize`**) | | POST | `/api/v1/admin_payouts/batches/{id}/process` | admin · `sensitive` | **unwired** — the irreversible step; no console button yet | | POST | `/api/v1/admin_payouts/{payoutId}/retry` | admin · `sensitive` | wired | | POST | `/api/v1/admin_payouts/{payoutId}/mark_failed` | admin · `sensitive` | **unwired** | | POST | `/api/v1/webhooks/payouts/{provider}` | **anonymous** · `webhook` | server-only — the transferor's reconciliation callback | This domain absorbed the **entire** wire-level drift between the 2026-07-13 contract freeze and the 2026-07-29 snapshot — one added endpoint and one changed schema, both here: - **Added (C-6):** `POST /api/v1/webhooks/payouts/{provider}` — the transferor's reconciliation callback. - **Changed (C-7):** `GeneratePayoutBatchCommand` gained **`systemInitiated: boolean`**, alongside the existing `periodStart`/`periodEnd` dates. That is the refinement-phase-7 scheduler flag: it distinguishes a batch the recurring job generated from one an admin generated, which is what keeps the "generation is automatic, processing is not" rule auditable. ### Phantom — 1 | Client call | REQ | Note | | --- | --- | --- | | `POST /api/v1/admin_payouts/{id}/transfer_reference` | REQ-036 | Deferred. The reference is *readable* on `PayoutDto.transferReference`; what is missing is a route to record one manually | ## Generation is automatic; processing is not This is a hard platform rule, not a convention: - A **scheduled job may generate** a `draft` batch. `IRecurringJob` + `RecurringJobSchedulerHostedService` do this weekly, in-process (refinement-phase-7). - The **irreversible `process` step is always an explicit admin action.** No job, no schedule, no retry loop may trigger a real transfer. Which is why `POST batches` is unwired (the job does it) and `POST batches/{id}/process` is unwired (the console has no button yet — that is the gap, and it is deliberate that nothing automated fills it). ## The money rules - **`UNIQUE` on booking id: one payout per booking, ever.** The DB constraint is the authority; the handler does not rely on an `if`. - **The net balance is SIGNED and must never be clamped to zero.** A nurse with a clawback larger than their eligible earnings has a **negative** `netAmountIrr`. Rendering it as 0 tells them they have nothing owing when in fact they owe. The client displays the signed value. - **Clawback netting is whole-clawback greedy**, never partial: a clawback either fits in this batch or waits for the next. See [refunds.md](refunds.md). - **Payout dates are resolved against the bank-holiday calendar server-side.** The client never computes one — see [admin.md](admin.md). `nurse_payout_interval_days` and `payout_satna_threshold_irr` are config; the threshold selects PAYA vs SATNA. - **A verified primary IBAN gates payment, not accrual.** `EligibleNurseEarningsDto.hasVerifiedPrimaryIban` is what the console checks; a nurse without one accrues a balance and is not paid. See [nurse.md](nurse.md). - The IBAN is **always masked** on every read model, nurse-side and admin-side alike. ## A resolved drift `payouts/apis/clientApi.ts` states that `NursePayoutHistoryDto` "carries **no** `failureReason` — that field lives on the admin-only `PayoutDto` … until REQ-025 adds it". **The live wire has it**: `failureReason` is on `NursePayoutHistoryDto` *and* `NursePayoutDetailDto` *and* `PayoutDto`. The comment predates the delivery; a `failed` payout can show its reason to the nurse today. The client also sends `Idempotency-Key` on process/retry. **The server does not read it there** — only `PaymentsController` and `CheckoutBnplController` do. Harmless (those writes are idempotent by constraint), but the header is decorative on this domain. See [../api-contract.md](../api-contract.md#idempotency). ## Enums | Vocabulary | Values | | --- | --- | | `PayoutStatus` | `pending` `submitted` `paid` `failed` | | `PayoutBatchStatus` | `draft` `processing` `partially_failed` `completed` `failed` | | `EarningsState` | `pending` `eligible` `paid` `clawback_applied` | `PayoutStatus` and `PayoutBatchStatus` are verified identical to `Entities/Payouts/*.cs` and are forward-only through `PayoutStatusTransitions`. `partially_failed` is a real batch outcome — some destinations settled, some did not — and drives the single-payout `retry`; `Seams:BankTransfer:FailIban` exists to make it testable. ## Open REQs | REQ | Status | Effect | | --- | --- | --- | | REQ-025 | delivered | The four-bucket balance, per-booking earnings list and payout detail are all served (including `failureReason`, above) | | REQ-036 | deferred | No single-payout preview, no `holidayShifted` flag, no record-transfer-reference route → 1 phantom | | REQ-053 | open | No payout forecast (next batch date + expected eligible amount). The nurse dashboard's forecast line renders nothing on the real path |