{
  "openapi": "3.1.0",
  "info": {
    "title": "GroundIT — PayrollPort (dhanavega EWA)",
    "version": "0.1.0",
    "license": {
      "name": "Proprietary — GroundIT/Sysmedac internal"
    },
    "description": "**Phase-2 / Proposed — no operation in this file ships at the India + KSA launch.** GroundIT's wire contract with dhanavega's abstract `PayrollPort` (ADR 0014): outbound calls GroundIT makes to dhanavega (`paths:`) and inbound HMAC-signed callbacks GroundIT receives from dhanavega (`webhooks:`), landing idempotently in `fintech.payroll_port_events` (db 13 §1). dhanavega remains system-of-record for the advance; GroundIT is the payroll/attendance signal source and at-source `SALARY_ADVANCE_REPAYMENT` deduction executor — never the lender (ADR 0014, ADR 0018). Tracks dhanavega ADR 0015 (Proposed). The concrete partner OpenAPI/sandbox has not landed — field names, the outbound base URL, and the OAuth2 client-credentials alternative to API-key+HMAC are **TBD**. See ../../09-surface-fintech-ports.md.\n"
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "GroundIT-hosted — the base for the inbound `webhooks:` callback below (dhanavega calls in).\n"
    },
    {
      "url": "https://partner.dhanavega.example/payrollport/v1",
      "description": "PLACEHOLDER — dhanavega-hosted PayrollPort base for the outbound `paths:` calls below (GroundIT calls out). Host, path prefix, and versioning are unconfirmed pending dhanavega's Phase-2 sandbox (ADR 0014 \"deferred, explicitly\"; tracked gap in architecture-docs/09-external-services-register.md).\n"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "outbound",
      "description": "Calls GroundIT initiates against dhanavega's PayrollPort (employer-side adapter, ADR 0014)."
    },
    {
      "name": "inbound",
      "description": "HMAC-signed callbacks dhanavega delivers to GroundIT, idempotent on the provider event id."
    }
  ],
  "x-reuse-anchors": {
    "responses": {
      "unauthorized": {
        "$ref": "#/components/responses/Unauthorized"
      },
      "not_found": {
        "$ref": "#/components/responses/NotFound"
      },
      "conflict": {
        "$ref": "#/components/responses/Conflict"
      },
      "unprocessable": {
        "$ref": "#/components/responses/UnprocessableEntity"
      },
      "locked": {
        "$ref": "#/components/responses/Locked"
      },
      "too_many": {
        "$ref": "#/components/responses/TooManyRequests"
      }
    },
    "parameters": {
      "idempotency_key": {
        "$ref": "#/components/parameters/IdempotencyKey"
      }
    },
    "headers": {
      "idem_replayed": {
        "$ref": "#/components/headers/IdempotencyReplayed"
      }
    }
  },
  "paths": {
    "/payrollport/employer-link-status": {
      "post": {
        "operationId": "fintech.payrollport.employer_link_status",
        "summary": "Pull employer/workspace link status from dhanavega",
        "description": "`employer.link_status` (ADR 0014, sync). GroundIT calls dhanavega to check whether the employer/ workspace behind `legal_entity_ref` is linked and active; the response lands an `EMPLOYER_LINK_STATUS` row in `fintech.payroll_port_events` and idempotently advances `fintech.ewa_enrolments.link_status` in the same request (db 13 §1). Backs the **Sync** action on FIN-S02 (fsd 12 §2.2). GroundIT projects this state; dhanavega decides it.\n",
        "tags": [
          "outbound"
        ],
        "x-token": "fintech.payrollport.employer_link_status",
        "x-realizes-features": [
          "FIN-F01"
        ],
        "x-screens": [
          "FIN-S02"
        ],
        "x-touches-entities": [
          "fintech.ewa_enrolments",
          "fintech.payroll_port_events",
          "org.legal_entities"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": null,
        "x-provisional": null,
        "security": [
          {
            "payrollPortClientAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmployerLinkStatusRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current employer/workspace link status, as landed in `payroll_port_events`.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmployerLinkStatusResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/payrollport/enrollment-sync": {
      "post": {
        "operationId": "fintech.payrollport.enrollment_sync",
        "summary": "Pull employee enrolment + KYC identity-match state from dhanavega",
        "description": "`enrollment.sync` (ADR 0014, sync). GroundIT calls dhanavega to pull the current KYC identity- match outcome and `provider_credit_limit_amount` for one enrolment; the response lands an `ENROLLMENT_SYNC` row in `fintech.payroll_port_events` and advances `fintech.ewa_enrolments.kyc_match_status`/`provider_credit_limit_amount` (db 13 §1). Backs FIN-S02 **Sync** and the FIN-S08 enrol step (fsd 12 §2.2, §3.2). GroundIT projects; dhanavega decides.\n",
        "tags": [
          "outbound"
        ],
        "x-token": "fintech.payrollport.enrollment_sync",
        "x-realizes-features": [
          "FIN-F01"
        ],
        "x-screens": [
          "FIN-S02",
          "FIN-S08"
        ],
        "x-touches-entities": [
          "fintech.ewa_enrolments",
          "fintech.payroll_port_events",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": null,
        "x-provisional": null,
        "security": [
          {
            "payrollPortClientAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrollmentSyncRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current enrolment/KYC state, as landed in `payroll_port_events`.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnrollmentSyncResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/payrollport/accrued-salary-feed": {
      "post": {
        "operationId": "fintech.payrollport.accrued_salary_feed",
        "summary": "Push the earned-wage signal to dhanavega (dynamic advance limit)",
        "description": "`accrued_salary.feed` (ADR 0014, async — runs on the jobs tier, XC-F08). GroundIT pushes one immutable `fintech.accrued_salary_feed` computation (earned wages to date, drawable headroom, the binding cap) so dhanavega can update the borrower's dynamic advance limit. `feed_status` moves `COMPUTED → PUSHED` on enqueue; the provider's `ACK`/`FAILED` outcome (dispatcher- bookkeeping only, db-docs/00 §12) is written directly onto the same row — it does NOT land in `payroll_port_events` (that table's `event_type` enum has no accrued-salary-feed value, db 13 §1). Surfaced on FIN-S04's Signal feed tab (fsd 12 §2.4).\n",
        "tags": [
          "outbound"
        ],
        "x-token": "fintech.payrollport.accrued_salary_feed",
        "x-realizes-features": [
          "FIN-F01"
        ],
        "x-screens": [
          "FIN-S04"
        ],
        "x-touches-entities": [
          "fintech.accrued_salary_feed",
          "fintech.ewa_enrolments"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "async",
        "x-emits-event": "fintech.accrued_salary_feed.pushed",
        "x-rls-scope": "tenant",
        "x-append-only": true,
        "x-entitlement": null,
        "x-provisional": null,
        "security": [
          {
            "payrollPortClientAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccruedSalaryFeedPushRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Push enqueued/transmitted; `feed_status` set to `PUSHED`. Poll FIN-S04 or subscribe to `fintech.accrued_salary_feed.pushed`.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccruedSalaryFeedPushResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/payrollport/deduction-confirmations": {
      "post": {
        "operationId": "fintech.payrollport.deduction_confirm",
        "summary": "Send at-source deduction reconciliation back to dhanavega",
        "description": "`deduction.confirm` (ADR 0014, async — the request leg). GroundIT reports a recovered or partially-recovered `fintech.salary_deductions` row back to dhanavega; this call only **transmits** the request. dhanavega's acknowledgement arrives later on the `deduction.confirm` inbound webhook below and is what flips `salary_deductions.status → CONFIRMED` + `confirmed_at` (db 13 §1). Backs FIN-S03 **Confirm to provider** (fsd 12 §2.3).\n",
        "tags": [
          "outbound"
        ],
        "x-token": "fintech.payrollport.deduction_confirm",
        "x-realizes-features": [
          "FIN-F01"
        ],
        "x-screens": [
          "FIN-S03"
        ],
        "x-touches-entities": [
          "fintech.salary_deductions"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "async",
        "x-emits-event": "fintech.salary_deduction.confirm_requested",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": null,
        "x-provisional": null,
        "security": [
          {
            "payrollPortClientAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeductionConfirmRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Confirmation request transmitted; awaiting the provider's async ack (inbound webhook).",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeductionConfirmRequestAck"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/payrollport/deep-link-sessions": {
      "post": {
        "operationId": "fintech.payrollport.deeplink_session_create",
        "summary": "Mint a shared-identity deep-link handoff into PayDay+ (Get advance)",
        "description": "Not one of the 5 `PayrollPort` capabilities — the ADR 0018 \"identity is shared/handed off\" seam for FIN-S08's **Get advance** CTA, modelled on the ADR 0010 bridge-token SSO pattern (short-lived, single-use, signed handoff token; 05-integration-architecture §2.3). No advance state is authored in GroundIT — dhanavega/PayDay+ is the system of record for the draw (db 13 §1 notes, ADR 0018). **G-14② decided (2026-07-02, design-docs/04)**: the deep-link contract stands; GroundIT authors no advance state — dhanavega keeps the system-of-record role for the draw.\n",
        "tags": [
          "outbound"
        ],
        "x-token": "fintech.payrollport.deeplink_session_create",
        "x-realizes-features": [
          "FIN-F01"
        ],
        "x-screens": [
          "FIN-S08"
        ],
        "x-touches-entities": [
          "fintech.ewa_enrolments",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "self",
        "x-append-only": false,
        "x-entitlement": null,
        "x-provisional": null,
        "security": [
          {
            "payrollPortClientAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeepLinkSessionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Single-use deep-link handoff minted (~5-minute TTL, mirrors the ADR 0010 bridge-token window).",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeepLinkSessionResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "webhooks": {
    "payrollPortEvent": {
      "post": {
        "operationId": "fintech.payrollport.event_receive",
        "summary": "Receive an inbound PayrollPort event from dhanavega (fast-ack)",
        "description": "HMAC-signed, idempotent inbound callback (mirrors `/platform/*` HMAC + fast-ack, 05-integration-architecture §2.2). Lands one of `EMPLOYER_LINK_STATUS` · `ENROLLMENT_SYNC` · `SALARY_POSTED` · `DEDUCTION_CONFIRM` into `fintech.payroll_port_events`, idempotent on `(tenant, provider, provider_event_id)` (db 13 §1 — a redelivery is a no-op). `SALARY_POSTED` is the ADR 0014 \"inbound webhook\" capability that triggers at-source deduction scheduling; `DEDUCTION_CONFIRM` here is dhanavega's async ack to `fintech.payrollport.deduction_confirm` above; `EMPLOYER_LINK_STATUS`/`ENROLLMENT_SYNC` MAY also arrive here unsolicited (dhanavega-side state change) rather than via the outbound pulls. Signature verified and stamped (`signature_valid`) before any state change; an unverified event is received but never applied (db 13 §1 check constraint). Responds fast (202); heavy processing is deferred to the jobs tier (XC-F08). Surfaced on FIN-S04's Events tab (fsd 12 §2.4).\n",
        "tags": [
          "inbound"
        ],
        "x-token": "fintech.payrollport.event_receive",
        "x-realizes-features": [
          "FIN-F01"
        ],
        "x-screens": [
          "FIN-S04"
        ],
        "x-touches-entities": [
          "fintech.payroll_port_events",
          "fintech.ewa_enrolments",
          "fintech.salary_deductions",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "async",
        "x-emits-event": "fintech.payroll_port_event.received",
        "x-rls-scope": "tenant",
        "x-append-only": true,
        "x-entitlement": null,
        "x-provisional": null,
        "security": [
          {
            "payrollPortHmacInbound": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayrollPortEventEnvelope"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Received and durably landed (idempotent); processing continues on the jobs tier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayrollPortEventAck"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "payrollPortClientAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-PayrollPort-Api-Key",
        "description": "GroundIT's own outbound credential presented to dhanavega — per-partner API key + request HMAC (ADR 0014: \"or OAuth2 client-credentials\", alternative TBD pending dhanavega's Phase-2 sandbox). Signing header/algorithm are unconfirmed; modelled here on the `/platform/*` HMAC shape (05-integration-architecture §2.2) pending the partner spec.\n"
      },
      "payrollPortHmacInbound": {
        "type": "apiKey",
        "in": "header",
        "name": "X-PayrollPort-Signature",
        "description": "HMAC signature GroundIT verifies on inbound dhanavega callbacks before any state change; outcome stamped on `fintech.payroll_port_events.signature_valid` (db 13 §1). Exact algorithm/header set TBD pending dhanavega's Phase-2 sandbox (ADR 0014) — modelled on `_shared.yaml#platformHmac`.\n"
      }
    },
    "schemas": {
      "BusinessNoRef": {
        "$ref": "#/components/schemas/BusinessNo"
      },
      "PayrollPortLinkStatus": {
        "type": "string",
        "enum": [
          "PENDING",
          "ACTIVE",
          "SUSPENDED",
          "UNLINKED"
        ],
        "description": "Mirrors `fintech.ewa_enrolments.link_status` (db 13 §1)."
      },
      "KycMatchStatus": {
        "type": "string",
        "enum": [
          "PENDING",
          "MATCHED",
          "MISMATCH",
          "EXPIRED"
        ],
        "description": "Mirrors `fintech.ewa_enrolments.kyc_match_status` (db 13 §1)."
      },
      "PayrollPortEventType": {
        "type": "string",
        "enum": [
          "EMPLOYER_LINK_STATUS",
          "ENROLLMENT_SYNC",
          "SALARY_POSTED",
          "DEDUCTION_CONFIRM"
        ],
        "description": "Mirrors `fintech.payroll_port_events.event_type` (db 13 §1)."
      },
      "AccruedSalaryFeedAckStatus": {
        "type": "string",
        "enum": [
          "ACK",
          "FAILED"
        ],
        "description": "The dispatcher-written outcome on `fintech.accrued_salary_feed.feed_status` (db 13 §1), excluding the pre-push `COMPUTED`/`PUSHED` states this call itself sets."
      },
      "DeductionReconStatus": {
        "type": "string",
        "enum": [
          "RECOVERED",
          "PARTIALLY_RECOVERED"
        ],
        "description": "The two `fintech.salary_deductions.status` values a confirmation reconciles (db 13 §1)."
      },
      "EmployerLinkStatusRequest": {
        "type": "object",
        "required": [
          "legal_entity_ref"
        ],
        "additionalProperties": false,
        "properties": {
          "legal_entity_ref": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ],
            "description": "GroundIT legal-entity business number identifying the employer/workspace."
          }
        }
      },
      "EmployerLinkStatusResponse": {
        "type": "object",
        "required": [
          "legal_entity_ref",
          "link_status",
          "provider_event_id",
          "checked_at"
        ],
        "properties": {
          "legal_entity_ref": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ]
          },
          "link_status": {
            "$ref": "#/components/schemas/PayrollPortLinkStatus"
          },
          "provider_event_id": {
            "type": "string",
            "description": "dhanavega's event id for this pull response; the `payroll_port_events.provider_event_id` idempotency key."
          },
          "checked_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "EnrollmentSyncRequest": {
        "type": "object",
        "required": [
          "enrolment_no"
        ],
        "additionalProperties": false,
        "properties": {
          "enrolment_no": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ],
            "description": "`fintech.ewa_enrolments.enrolment_no` being synced."
          }
        }
      },
      "EnrollmentSyncResponse": {
        "type": "object",
        "required": [
          "enrolment_no",
          "kyc_match_status",
          "provider_event_id",
          "synced_at"
        ],
        "properties": {
          "enrolment_no": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ]
          },
          "provider_borrower_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "dhanavega-side borrower/account id."
          },
          "kyc_match_status": {
            "$ref": "#/components/schemas/KycMatchStatus"
          },
          "provider_credit_limit_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Money"
              },
              {
                "type": "null"
              }
            ]
          },
          "provider_event_id": {
            "type": "string"
          },
          "synced_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "AccruedSalaryFeedPushRequest": {
        "type": "object",
        "description": "One `fintech.accrued_salary_feed` computation (db 13 §1), pushed verbatim.",
        "required": [
          "enrolment_no",
          "as_of_date",
          "pay_period_start",
          "pay_period_end",
          "earned_wages_amount",
          "max_advance_pct",
          "outstanding_advance_amount",
          "eligible_amount"
        ],
        "additionalProperties": false,
        "properties": {
          "enrolment_no": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ]
          },
          "employee_ref": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ],
            "description": "`people.employees.employee_no` (denormalized)."
          },
          "as_of_date": {
            "$ref": "#/components/schemas/DateOnly"
          },
          "pay_period_start": {
            "$ref": "#/components/schemas/DateOnly"
          },
          "pay_period_end": {
            "$ref": "#/components/schemas/DateOnly"
          },
          "earned_wages_amount": {
            "$ref": "#/components/schemas/Money"
          },
          "max_advance_pct": {
            "$ref": "#/components/schemas/Rate"
          },
          "outstanding_advance_amount": {
            "$ref": "#/components/schemas/Money"
          },
          "employer_cap_amount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Money"
              },
              {
                "type": "null"
              }
            ]
          },
          "eligible_amount": {
            "$ref": "#/components/schemas/Money"
          }
        }
      },
      "AccruedSalaryFeedPushResponse": {
        "type": "object",
        "required": [
          "feed_status",
          "pushed_at"
        ],
        "properties": {
          "feed_status": {
            "$ref": "#/components/schemas/AccruedSalaryFeedAckStatus"
          },
          "provider_receipt_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "pushed_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "DeductionConfirmRequest": {
        "type": "object",
        "required": [
          "deduction_no",
          "provider_advance_ref",
          "recovery_period",
          "scheduled_amount",
          "recovered_amount",
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "deduction_no": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ],
            "description": "`fintech.salary_deductions.deduction_no`."
          },
          "provider_advance_ref": {
            "type": "string",
            "description": "dhanavega-side advance id this repayment settles."
          },
          "recovery_period": {
            "$ref": "#/components/schemas/DateOnly"
          },
          "scheduled_amount": {
            "$ref": "#/components/schemas/Money"
          },
          "recovered_amount": {
            "$ref": "#/components/schemas/Money"
          },
          "status": {
            "$ref": "#/components/schemas/DeductionReconStatus"
          }
        }
      },
      "DeductionConfirmRequestAck": {
        "type": "object",
        "required": [
          "transmitted",
          "deduction_no"
        ],
        "properties": {
          "transmitted": {
            "type": "boolean"
          },
          "deduction_no": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ]
          }
        }
      },
      "DeepLinkSessionRequest": {
        "type": "object",
        "required": [
          "employee_ref",
          "purpose"
        ],
        "additionalProperties": false,
        "properties": {
          "employee_ref": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              }
            ]
          },
          "purpose": {
            "type": "string",
            "enum": [
              "EWA_DRAW"
            ],
            "description": "FIN-S08 \"Get advance\" is the only handoff purpose at this depth."
          }
        }
      },
      "DeepLinkSessionResponse": {
        "type": "object",
        "required": [
          "deep_link_url",
          "session_token",
          "expires_at"
        ],
        "properties": {
          "deep_link_url": {
            "type": "string",
            "format": "uri"
          },
          "session_token": {
            "type": "string",
            "description": "Single-use signed handoff token (ADR 0010 bridge-token pattern)."
          },
          "expires_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "PayrollPortEventEnvelope": {
        "type": "object",
        "description": "The verified raw provider event body, kept for audit (db 13 §1 JSONB payload shape); shape of `payload` varies by `event_type`.",
        "required": [
          "provider_event_id",
          "event_type",
          "occurred_at",
          "payload"
        ],
        "additionalProperties": false,
        "properties": {
          "provider_event_id": {
            "type": "string",
            "description": "dhanavega's event id — the idempotency key."
          },
          "event_type": {
            "$ref": "#/components/schemas/PayrollPortEventType"
          },
          "legal_entity_ref": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              },
              {
                "type": "null"
              }
            ]
          },
          "employee_ref": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/BusinessNo"
              },
              {
                "type": "null"
              }
            ]
          },
          "occurred_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "payload": {
            "type": "object",
            "additionalProperties": true,
            "description": "Raw event body, e.g. for `SALARY_POSTED` (db 13 §1): `{ event_id, employer_ref, employee_ref, advance_ref, posted_amount, pay_period: { start, end }, currency }`.\n"
          }
        }
      },
      "PayrollPortEventAck": {
        "type": "object",
        "required": [
          "received",
          "provider_event_id"
        ],
        "properties": {
          "received": {
            "type": "boolean"
          },
          "provider_event_id": {
            "type": "string"
          },
          "idempotency_replayed": {
            "type": "boolean",
            "description": "true when `provider_event_id` had already landed (redelivery no-op, db 13 §1)."
          }
        }
      },
      "BusinessNo": {
        "type": "string",
        "description": "Tenant-unique, prefixed human reference (`employee_no`, `requisition_no`, `offer_no`, `payslip_no`, `claim_no`, `ticket_no`, `asset_no`, `case_no`). Read-model field; never a path key.\n"
      },
      "Timestamp": {
        "type": "string",
        "format": "date-time",
        "description": "timestamptz, serialized UTC ISO-8601. Presentation timezone is a client concern."
      },
      "Money": {
        "type": "object",
        "description": "Exact decimal money (db-docs/00 §6). `amount` is a STRING so float never enters the wire.",
        "required": [
          "amount",
          "currency_code"
        ],
        "additionalProperties": false,
        "properties": {
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d{1,2})?$",
            "description": "numeric(18,2) as a string, e.g. \"45000.00\"."
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCode"
          }
        }
      },
      "DateOnly": {
        "type": "string",
        "format": "date",
        "description": "Calendar-only value (pay-period date, leave date, due date)."
      },
      "Rate": {
        "type": "string",
        "pattern": "^-?\\d+(\\.\\d{1,6})?$",
        "description": "numeric(9,6) fraction as a string, e.g. \"0.120000\" for the 12% EPF rate. Never a float; percentages are stored as fractions."
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail (application/problem+json). The platform-wide error envelope (03 §1).",
        "required": [
          "type",
          "title",
          "status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "default": "about:blank",
            "description": "Problem-type URI."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary (stable per type)."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code, duplicated for convenience."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation specific to this occurrence."
          },
          "instance": {
            "type": "string",
            "description": "URI reference for this specific occurrence."
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "correlation_id": {
            "$ref": "#/components/schemas/Uuid"
          }
        }
      },
      "ValidationProblem": {
        "description": "422 field-level validation failure; extends Problem with a per-field error array. `detail` is ALWAYS present on a 422 (#1251) and is the human summary of `errors[]`: one offending field renders as `\"<field>: <its message>\"` (`withholding_amount: is required for an India entity`), several as `\"N fields were refused: a, b, c.\"`, capped at five names. It is display copy derived from members already in the same body — clients keep branching on `code` and mapping `errors[].pointer` back to a control, never parsing this sentence.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/Problem"
          },
          {
            "type": "object",
            "required": [
              "detail",
              "errors"
            ],
            "properties": {
              "detail": {
                "type": "string",
                "description": "Human summary of `errors[]`, always populated on a 422 so a client never has to fall back to generic copy for the one status that names a fixable field.\n",
                "example": "withholding_amount: is required for an India entity"
              },
              "errors": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "pointer",
                    "rule"
                  ],
                  "properties": {
                    "pointer": {
                      "type": "string",
                      "description": "JSON Pointer to the offending field, e.g. /claim_amount"
                    },
                    "rule": {
                      "type": "string",
                      "enum": [
                        "required",
                        "format",
                        "length",
                        "range",
                        "cross-field",
                        "async-server",
                        "consent-gated",
                        "uniqueness-business",
                        "not_found"
                      ],
                      "description": "FSD validation taxonomy rule (fsd-docs/00 §8.2). `not_found` is the server-side-lookup arm: a body field that REFERENCES another resource (e.g. `project_id` on a work entry) and did not resolve for this caller. It is reported here, under the field's pointer, and NOT as a 404 — the request addresses its own resource, so the failure belongs on the form field the client can actually fix (#805).\n"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "CurrencyCode": {
        "type": "string",
        "enum": [
          "INR",
          "SAR"
        ],
        "description": "ISO-4217. A legal entity operates in a single currency (db-docs/00 §6)."
      },
      "ErrorCode": {
        "type": "string",
        "description": "Machine-stable error code (paired with the HTTP status; drives client handling, not display copy).",
        "enum": [
          "VALIDATION_FAILED",
          "IDEMPOTENCY_KEY_REUSE",
          "VERSION_CONFLICT",
          "PRECONDITION_REQUIRED",
          "MAKER_EQUALS_CHECKER",
          "STEP_UP_REQUIRED",
          "CONSENT_REQUIRED",
          "TOKEN_DENIED",
          "SCOPE_DENIED",
          "OWNERSHIP_DENIED",
          "STATE_TRANSITION_INVALID",
          "FACE_MISMATCH",
          "FACE_VERIFICATION_REQUIRED",
          "WITHHOLDING_REVIEW_REQUIRED",
          "FEATURE_NOT_IN_PLAN",
          "PLAN_LIMIT_EXCEEDED",
          "TENANT_PAST_DUE",
          "TENANT_SUSPENDED",
          "TENANT_CANCELLED",
          "RATE_LIMITED",
          "NOT_FOUND"
        ]
      },
      "Uuid": {
        "type": "string",
        "format": "uuid",
        "description": "UUIDv7 surrogate primary key (db-docs/00 §3). Never the business identifier."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, expired, or invalid session token (no authenticated principal).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource does not exist OR is masked by RLS (tenant/self/team/branch scope) — the API does not distinguish, so existence is never confirmed across a scope boundary (02 §4 disclosure posture).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Conflict": {
        "description": "Idempotency-key reuse with a different body, an invalid state transition, or a uniqueness/business-rule violation (fsd 00 §8.2). For database-backed uniqueness rules, detail names the exact violated rule; unmapped constraints are not collapsed into a generic 409.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "Field-level validation failed. The body carries both `errors[]` (per-field, pointer-addressed) and a `detail` summarising them — never an empty `detail` (#1251).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationProblem"
            },
            "examples": {
              "singleField": {
                "summary": "One refused field — `detail` names it",
                "value": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "withholding_amount: is required for an India entity",
                  "errors": [
                    {
                      "pointer": "/withholding_amount",
                      "rule": "required",
                      "message": "is required for an India entity"
                    }
                  ]
                }
              },
              "severalFields": {
                "summary": "Several refused fields — `detail` counts and names them",
                "value": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "2 fields were refused: effective_from, cap_amount.amount.",
                  "errors": [
                    {
                      "pointer": "/effective_from",
                      "rule": "cross-field",
                      "message": "Overlaps an existing version."
                    },
                    {
                      "pointer": "/cap_amount/amount",
                      "rule": "range",
                      "message": "Must be a non-negative decimal string."
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "Locked": {
        "description": "Tenant subscription `past_due` (ADR 0009): WRITES are blocked (423), reads still succeed. `code` = TENANT_PAST_DUE. Mutations return this; list/get operations do not.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Per-principal/route rate limit exceeded.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Client-generated unique key. REQUIRED on every mutation (this round tightens ADR 0015's \"platform + retryable mutations\" floor to ALL mutations for uniformity — offline punch/leave sync depends on it). Scoped (tenant, principal, route, key); a replay within the ~24h window returns the stored response with `Idempotency-Replayed: true`; the same key with a different body → 409 IDEMPOTENCY_KEY_REUSE (04 §1).\n",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 255
        }
      }
    },
    "headers": {
      "IdempotencyReplayed": {
        "description": "`true` when a stored idempotent response was replayed rather than freshly computed.",
        "schema": {
          "type": "boolean"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying (on 429 / 503).",
        "schema": {
          "type": "integer"
        }
      }
    }
  }
}