# Contract — Payments core: ledger, transactions, webhooks & card capture (backend phase b10) > One-line: the inbound money rail — start a card payment against an accepted request, a PSP webhook confirms > it, the balanced card-capture ledger group posts, and the booking confirms. Assumes > [`../conventions/api-conventions.md`](../conventions/api-conventions.md) + > [`../conventions/money-and-types.md`](../conventions/money-and-types.md). Machine schema: > [`../openapi/swagger.v1.json`](../openapi/README.md). **Status:** live as of backend-phase-b10 · **Frontend consumer:** frontend-phase-f9-b10 All money is **IRR Rials, integer, on the wire as a string of digits** (`"23300000"`). The card-capture ledger group is always **balanced** (Σ debit = Σ credit). **Internal `account_type`s are never exposed to the customer** — the checkout UI shows gross + the commission/VAT breakdown only. Timestamps are UTC ISO-8601. ## Enums used - `payment` status (`payment_transactions.status`): `pending` | `succeeded` | `failed`. - `payment_gateways.type`: `standard` (card IPG) | `bnpl`. - `payment_webhook_events.processing_status`: `received` | `processed` | `failed` | `ignored`. - `account_type` (internal, never on the customer wire): `escrow_held` | `platform_revenue` | `nurse_payable` | `refund_payable` | `bnpl_fee_expense` | `psp_fee_expense` | `nurse_clawback_receivable` | `bad_debt`. b10 posts only the first three (card capture); the rest are reserved for b11/b12/b13. ## Endpoints ### `POST api/v1/bookings/{bookingRequestId}/payments` - **Purpose:** start a card payment for an `accepted_awaiting_payment` booking request owned by the caller. A `bookings` row exists only on capture (b9), so payment is initiated against the **request**; the amount charged is the request's **frozen gross** (variant price × session count), never client-supplied. - **Auth:** authenticated (the owning customer) · **Rate-limited:** yes (sensitive) · **Idempotency:** send an **`Idempotency-Key`** header — a retried start reuses the same attempt/reference. - **Request body:** none (the id is in the route; the key is a header). - **Success `200` (`data`):** `{ "transactionId": 42, "redirectUrl": "https://…", "gatewayReferenceCode": "…" }`. No ledger rows yet; the booking is not created yet. - **Failure:** `400` bad id, `401` unauth, `404` request not found / not the caller's, `409` already paid / not awaiting payment / payment window lapsed, `400` no active gateway configured. ### `POST api/v1/webhooks/payments/{provider}` - **Purpose:** the inbound PSP/BNPL callback — verify-then-dedup-then-mutate. - **Auth:** **none (signature-authenticated)**, anonymous to the auth pipeline · **Rate-limited:** yes (global per-IP) · **Idempotent:** yes, at-least-once tolerant. - **Request:** the raw provider callback body (stored verbatim in `payload_json`); signature material in headers. - **Behaviour:** upserts `payment_webhook_events` **first** on `(provider, external_event_id)` and **no-ops on a duplicate**; an **invalid signature** is stored `ignored` and mutates nothing; on a **new success event** it re-verifies server-side (never trusts the callback alone), then captures — posts the balanced card-capture group and creates/confirms the booking — all under a `lock(booking:{id}:payment)` with the DB uniques as the authoritative backstop. - **Success `200` (`data`):** `{ "processingStatus": "processed" | "ignored" | "failed", "duplicate": false }` (`duplicate: true` on a replayed event). ### `GET api/v1/nurses/{nurseId}/payable_balance` - **Purpose:** the IRR balance currently owed a nurse — the **signed sum** over `nurse_payable` ledger legs (credit adds, debit subtracts), **derived, never a stored column**. This is what b13 payouts read. - **Auth:** authenticated — the **nurse themself or an admin/finance role** (`403` otherwise). - **Success `200` (`data`):** `{ "nurseId": 7, "balanceIrr": "19805000" }`. ## The card-capture ledger group (posted on webhook confirm) One `transaction_group_id`, `amount_irr` positive with `direction` carrying the sign, Σdebit = Σcredit: ``` DEBIT escrow_held gross_price_irr (e.g. 23300000) CREDIT platform_revenue balinyaar_commission_irr (e.g. 3495000) CREDIT nurse_payable nurse_payout_amount (e.g. 19805000, nurse_id set) ``` ## Load-bearing rules the client must honour - **Money is IRR integer, on the wire as a digit-string.** Never coerce to a JS number for math. - **A booking is created & confirmed on capture** (the webhook), not on initiate — after `initiate` the redirect is shown; the booking appears once the PSP callback confirms. - **The checkout shows gross + commission/VAT breakdown only** — never the internal `account_type`s. - **Payment is idempotent end-to-end**: a retried `initiate` (same `Idempotency-Key`) reuses the attempt; a replayed webhook is a no-op; a repeat `initiate` after capture is a `409`.