{
  "openapi": "3.0.1",
  "info": {
    "title": "I am alive api release-date:#07-10-2026 06:01:47",
    "description": "Backend for I Am Alive pensioner verification: the retiree web and mobile apps, and the admin portal.\r\n\r\n**Tokens.** Send `Authorization: Bearer {token}`.\r\n- Retirees: `POST /api/Retirees/requestOtp`, then `POST /api/Retirees/VerifyOtp`. Valid for 30 minutes.\r\n- Staff: `POST /api/Auth/admin-login`, then `POST /api/Auth/admin-verify-2fa`. Valid for 15 minutes.\r\n- One session per user: signing in again makes that user's older tokens return 401.\r\n\r\n**Responses** mostly use `{ \"isError\", \"responseCode\", \"message\", \"data\" }`. `responseCode` `\"0\"` means success and failures use the codes listed on each endpoint. A few endpoints set `responseCode` inconsistently or return a bare object; their descriptions say so, and `isError` is the safer check.\r\n\r\n**Encrypted requests.** Endpoints marked 🔒 take an AES-encrypted payload. Each documents the model inside it, and the /docs page encrypts *Try it out* requests for you.\r\n\r\n**Organisations.** Admin endpoints only see the caller's organisation, unless their description says otherwise.",
    "termsOfService": "https://lotteryengine.com",
    "contact": {
      "name": "Daddywa & co",
      "url": "https://www.linkedin.com/in/timilehin-ogunseye-466bb259/",
      "email": "ogunseye.timilehin@gmail.com"
    },
    "license": {
      "name": "Use under LICX",
      "url": "https://www.linkedin.com/in/timilehin-ogunseye-466bb259/"
    },
    "version": "v1"
  },
  "paths": {
    "/api/Admin/accounts": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Lists all staff user accounts.",
        "description": "Every staff user, newest first, with organisation name, role, email and phone, and whether the account is active. Not limited to the caller's organisation.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "responses": {
          "200": {
            "description": "The users."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/account/update": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Updates the signed-in staff user's own name and contact details.",
        "description": "Self-service: replaces the first name, last name, email and phone number of the user in the token.\r\n            \r\n- `id` can be left out. If sent, it must be the caller's own ID.\r\n- `role` can be left out. If sent, it must be the caller's current role.\r\n- `organisationId` is ignored.\r\n            \r\nEditing another user or changing a role is refused: only Admins can do that, with `POST /api/Admin/user/update`.\r\n            \r\n**Errors:**\r\n- 403, `-3`: `id` is someone else's, or `role` isn't the caller's current role. Nothing is changed.\r\n- 400, `-l`: the caller's account wasn't found, or the update was rejected (for example, an invalid email).\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.\n\n**Encrypted request:** shown here is the `UserEditDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserEditDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Updated."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "403": {
            "description": "Tried to edit another user or change a role."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "UserEditDto",
          "type": "object"
        }
      }
    },
    "/api/Admin/data/matching-report": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Matching reports in a date range, for the dashboard.",
        "description": "Returns the organisation's matching reports: how the BVN/NIN provider's record and photo compared with each retiree's details and selfie.\r\n            \r\n**Known issue:** the date filter keeps reports created on or before `StartDate`, not those between the two dates. If anything fails, returns an empty list rather than an error.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "StartDate",
            "in": "query",
            "description": "Start of the range.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "EndDate",
            "in": "query",
            "description": "End of the range.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reports."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/data/cards": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Dashboard headline numbers for the current verification period.",
        "description": "For the caller's organisation. For its current verification period: total verifications and their share of all retirees, successful and failed counts with their share of verifications, and the total number of retirees. Percentages are strings.\r\n            \r\n`year` is ignored. The share of retirees counts at most 10,000 retirees.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "description": "Ignored.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The numbers."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/data/pie": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Successful and failed verifications in a year, for a pie chart.",
        "description": "For the caller's organisation. `data.totals` is `[successful, failed]` for `year`. If the query fails, returns `[0, 0, 0]` as a success.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "description": "Calendar year, for example 2026.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The totals."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/data/bar": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Successful and failed verifications per month, for a bar chart.",
        "description": "For the caller's organisation. `data.months` is Jan to Dec, and `data.successful` and `data.failed` each have 12 monthly counts for `year`.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "description": "Calendar year, for example 2026.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The monthly counts."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/data/quarterly": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Successful verifications per quarter, as a share of all retirees.",
        "description": "For the caller's organisation. For each quarter of `year`, `q1` to `q4` give `fraction` (successful out of retirees), `percent`, `data` (the percentage as a number) and `trend` (`up`, `down` or `flat` compared with the previous quarter; empty for Q1).\r\n            \r\nCounts at most 10,000 retirees. Returns an empty object when the organisation has no retirees.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "description": "Calendar year, for example 2026.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The quarters."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/data/donutchart": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "All-time verification totals by outcome, for a donut chart.",
        "description": "For the caller's organisation. `data.totals` is `[successful, validated, failed]` across all periods.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "responses": {
          "200": {
            "description": "The totals."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/verificationperiods": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Lists the organisation's verification periods.",
        "description": "For the caller's organisation. Paginated. `search` matches the text of the from and to dates.\r\n            \r\nA verification period is the window in which retirees can verify. Outside one, OTP requests and liveness checks are refused.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 10
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Text to look for in the dates.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of periods."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      },
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Creates a verification period.",
        "description": "Send `fromDate` and `toDate`. The period is created for the caller's organisation, and any `organisationId` in the payload is replaced. Retirees can verify on any day from `fromDate` to `toDate`.\r\n            \r\n**Errors (400):** `isError` is true, `data` is false and `message` has the reason. `responseCode` is `\"0\"` even then, so check `isError`.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin or Manager.\n\n**Encrypted request:** shown here is the `VerificationPeriod` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerificationPeriod"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Created."
          },
          "400": {
            "description": "Failed; see `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "VerificationPeriod",
          "type": "object"
        }
      }
    },
    "/api/Admin/operations/verificationperiods/update": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Changes a verification period's dates.",
        "description": "Send `id`, `fromDate` and `toDate`. `organisationId` is replaced with the caller's organisation.\r\n            \r\n**Errors (400):** as for creating a period, `responseCode` is `\"0\"` even on failure, so check `isError`.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin or Manager.\n\n**Encrypted request:** shown here is the `VerificationPeriod` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerificationPeriod"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Updated."
          },
          "400": {
            "description": "Failed; see `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "VerificationPeriod",
          "type": "object"
        }
      }
    },
    "/api/Admin/operations/verifications/{id}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Verifications filtered by outcome, with the images behind each.",
        "description": "For the caller's organisation. `id` filters by status: `1` failed, `2` validated, `3` successful. Any other value, or none, returns all of them. Not paginated.\r\n            \r\nEach item has the pension ID, name, status, BVN and NIN (not masked), the captured selfie's path, the government ID photo's path, and `basePath` for building image URLs.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Status to filter by: 1 failed, 2 validated, 3 successful. Omit for all.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The verifications."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/verifications/byretireeNo/{retiree_no}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "All verification attempts for one pension ID.",
        "description": "The raw verification records (date, status and period) for `retiree_no`, in no particular order. Not limited to the caller's organisation.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "retiree_no",
            "in": "path",
            "description": "Pension ID.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The attempts."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/verifications": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Paginated verifications with images, optionally for the latest period only.",
        "description": "For the caller's organisation. `search` matches the pension ID, names, BVN or NIN.\r\n            \r\n- Without `verificationPeriod`: verifications from the last 5 years.\r\n- With any `verificationPeriod`: verifications in the organisation's most recent period.\r\n            \r\n**Known issue:** the `verificationPeriod` value itself is ignored, so an earlier period can't be picked.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "verificationPeriod",
            "in": "query",
            "description": "Any value limits results to the latest period (see the known issue).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 20
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Pension ID, name, BVN or NIN to look for.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of verifications."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/updated-retirees": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Retirees whose BVN or NIN has been confirmed.",
        "description": "For the caller's organisation. Retirees with a confirmed ID, set after a successful liveness check or when `POST /api/Admin/operations/retireeId-status-update` accepts it. BVN and NIN are masked to the last 4 digits. Includes contact details, next of kin and the last verification.\r\n            \r\nOnly the first 10,000 retirees are checked.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "responses": {
          "200": {
            "description": "The retirees."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/retireeId-status-update": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Accepts or rejects a retiree's BVN or NIN.",
        "description": "Send `retireeNo`, `identityType` (`BVN` or `NIN`), `identityNumber` and `updateStatus`:\r\n            \r\n- `2` accepts: saves `identityNumber` as the retiree's BVN or NIN, makes it their identity type and marks the ID as confirmed. Later liveness checks use it without asking.\r\n- `1` rejects: clears that BVN or NIN and marks the ID as unconfirmed.\r\n            \r\nThe retiree's latest matching report is marked `successful` or `unsuccessful` to match.\r\n            \r\nA blank `identityNumber` keeps the BVN or NIN already on record.\r\n            \r\n**Errors (400):**\r\n- `-1` Pension ID not found, or the update failed.\r\n- `-2` Accepting: another pensioner in the organisation already has that BVN or NIN.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.\n\n**Encrypted request:** shown here is the `UpdatedIDStatusDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdatedIDStatusDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Accepted or rejected."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "UpdatedIDStatusDto",
          "type": "object"
        }
      }
    },
    "/api/Admin/operations/matching-reports": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Paginated matching reports.",
        "description": "For the caller's organisation. Each matching report records how the BVN/NIN provider's record and photo compared with a retiree's details and selfie. `search` matches the pension ID, names, BVN, NIN or remark.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 10
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Pension ID, name, BVN, NIN or remark to look for.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of reports."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/matching-reports/{id}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "One matching report by its ID.",
        "description": "**Known issue:** this route has the same shape as `GET /api/Admin/operations/matching-reports/{retireeid}`, so every request to either fails with 500 (ambiguous route) until one of them is renamed.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Matching report ID.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/matching-reports/{retireeid}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "The latest matching report for a retiree.",
        "description": "Meant to take the retiree's database ID.\r\n            \r\n**Known issue:** this route has the same shape as `GET /api/Admin/operations/matching-reports/{id}`, so every request to either fails with 500 (ambiguous route) until one of them is renamed. The `{retireeid}` route value also doesn't reach the method's `id` parameter.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "description": "Retiree database ID (see the known issue).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "retireeid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/retirees": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Paginated list of the organisation's retirees.",
        "description": "For the caller's organisation. `search` matches the pension ID, names, email, phone, BVN or NIN. BVN and NIN are masked to the last 4 digits. Each retiree includes contact details, next of kin, date of birth, and the last verification's date and status.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 10
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Pension ID, name, email, phone, BVN or NIN to look for.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of retirees."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/retirees/xlsx": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Downloads the organisation's retirees as an Excel file.",
        "description": "For the caller's organisation. `Retirees.xlsx`, with one row per retiree (up to 100,000): pension ID, names, email, date of birth, phone, BVN and NIN (masked to the last 4 digits), address, and both next of kin.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager or Supervisor.",
        "responses": {
          "200": {
            "description": "The spreadsheet."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/reports": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Verifications between two dates.",
        "description": "For the caller's organisation. `StartDate` and `EndDate` are dates such as `2026-01-31`; both days are included. Each item has the pension ID, name, status, and BVN and NIN (not masked). Not paginated.\r\n            \r\n**Errors (400):**\r\n- `-1` Missing or unreadable dates.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager or Supervisor.",
        "parameters": [
          {
            "name": "StartDate",
            "in": "query",
            "description": "First day to include, such as 2026-01-01.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "EndDate",
            "in": "query",
            "description": "Last day to include, such as 2026-01-31.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The verifications."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/operations/reports/xlsx": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Downloads the date-range verification report as an Excel file.",
        "description": "The same data as `GET /api/Admin/operations/reports`, as `VerificationReport.xlsx`.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager or Supervisor.",
        "parameters": [
          {
            "name": "StartDate",
            "in": "query",
            "description": "First day to include, such as 2026-01-01.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "EndDate",
            "in": "query",
            "description": "Last day to include, such as 2026-01-31.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The spreadsheet."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "deprecated": true
      }
    },
    "/api/Admin/notification/messages": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Messages sent to retirees.",
        "description": "For the caller's organisation. Its email and SMS messages, optionally between `fromDate` and `toDate`, with subject, body, type, audience, status and the units used.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager or Supervisor.",
        "parameters": [
          {
            "name": "fromDate",
            "in": "query",
            "description": "Earliest send date.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "toDate",
            "in": "query",
            "description": "Latest send date.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The messages."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/notification/messages/usage-statistics": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Email and SMS units allowed and used.",
        "description": "For the caller's organisation. `emailUnit` and `smsUnit` are its allowances (`-1` means unlimited); `emailUnitUsed` and `smsUnitUsed` are what's been used so far.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager or Supervisor.",
        "responses": {
          "200": {
            "description": "The usage."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/notification/messages/send": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Sends an email or SMS to a group of retirees.",
        "description": "Send `multipart/form-data` with:\r\n- `payload`: `subject`, `message`, `messageType` (`Email` or `Sms`) and `sendTo`.\r\n- `File`: only for `SelectedRetirees`. A CSV of the retirees to message, with `RetireeNumber`, `Phone` and `Email` columns (see `GET /api/Admin/notification/template.csv`). A row matches a retiree on any of the three.\r\n            \r\n`sendTo` is `AllRetirees` (everyone in the organisation), `UnverifiedRetirees` (not yet verified in the current period) or `SelectedRetirees` (from the CSV).\r\n            \r\nAn email costs 1 unit per recipient; an SMS costs 1 unit per 160 characters per recipient. The request is refused if the organisation doesn't have enough units left. Accepted messages are queued and sent in the background.\r\n            \r\n**Errors (400):**\r\n- `-1` With the reason: no current period, not enough units, or a missing or invalid CSV.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager or Supervisor.\n\n**Encrypted request:** shown here is the `MessageDto` inside the payload. On the wire, the `payload` form field carries the ciphertext of this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "payload": {
                    "$ref": "#/components/schemas/MessageDto"
                  },
                  "ContentType": {
                    "type": "string"
                  },
                  "ContentDisposition": {
                    "type": "string"
                  },
                  "Headers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "Length": {
                    "type": "integer",
                    "format": "int64"
                  },
                  "Name": {
                    "type": "string"
                  },
                  "FileName": {
                    "type": "string"
                  }
                }
              },
              "encoding": {
                "payload": {
                  "style": "form"
                },
                "ContentType": {
                  "style": "form"
                },
                "ContentDisposition": {
                  "style": "form"
                },
                "Headers": {
                  "style": "form"
                },
                "Length": {
                  "style": "form"
                },
                "Name": {
                  "style": "form"
                },
                "FileName": {
                  "style": "form"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Queued."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "form",
          "name": "payload",
          "model": "MessageDto",
          "type": "object"
        }
      }
    },
    "/api/Admin/notification/template.csv": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Sample rows for the recipients CSV used when messaging selected retirees.",
        "description": "Returns two example rows with `retireeNumber`, `phone` and `email`: the columns `POST /api/Admin/notification/messages/send` reads from its CSV. Despite the `.csv` name, the response is JSON. Served at both `GET /api/Admin` and `GET /api/Admin/notification/template.csv`.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin or Manager.",
        "responses": {
          "200": {
            "description": "The sample rows."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Sample rows for the recipients CSV used when messaging selected retirees.",
        "description": "Returns two example rows with `retireeNumber`, `phone` and `email`: the columns `POST /api/Admin/notification/messages/send` reads from its CSV. Despite the `.csv` name, the response is JSON. Served at both `GET /api/Admin` and `GET /api/Admin/notification/template.csv`.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin or Manager.",
        "responses": {
          "200": {
            "description": "The sample rows."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/onboard/preview": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Checks an onboarding spreadsheet and shows what it would import, without saving anything.",
        "description": "Send the same `multipart/form-data` as `POST /api/Admin/onboard/upload`. The file goes through every check the upload makes, including whether pension IDs are repeated in the file or already in the system, and nothing is saved.\r\n            \r\n`data` holds:\r\n- `canImport`: true when the file has no problems, so uploading it will import every row.\r\n- `totalRows`, `rowsWithProblems`, and `hasHeaderRow` (a header row is skipped, not imported).\r\n- `warnings`, such as columns the template doesn't have, which are ignored and listed in `ignoredColumns`.\r\n- `errors`: problems with the file as a whole, such as a missing required column.\r\n- `rows`: each row as it would be saved, with its own `errors`. Rows with problems come first, then the rest, each in sheet order. BVN and NIN are masked to the last 4 digits. The first 500 rows are included, plus every row with a problem.\r\n            \r\nEach row's `identityType` is the ID the pensioner will verify with: `BVN` when the row has a BVN, `NIN` when it has only a NIN, and `null` when it has neither (the pensioner picks one at their first verification). Rows with both have `canChooseIdentityType` true; they verify with the BVN unless their pension ID is sent in `VerifyWithNin` with the upload.\r\n            \r\nEach row is checked for: pension ID, names and date of birth present; a real date of birth, not in the future; BVN and NIN of exactly 11 digits; a valid email; well-formed phone numbers; and pension IDs, BVNs and NINs not repeated in the file or already held by another pensioner.\r\n            \r\nA file with problems still returns 200; check `canImport`.\r\n            \r\n**Errors (400):**\r\n- `-1` Missing file, wrong type or size, or a file that can't be read.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin or Manager.",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "OrganisationId": {
                    "type": "string"
                  },
                  "ExcelFile": {
                    "type": "string",
                    "format": "binary"
                  },
                  "VerifyWithNin": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Pension IDs of pensioners with both a BVN and a NIN who should verify with their NIN.\r\nThey verify with their BVN otherwise. Send one form field per pension ID."
                  }
                }
              },
              "encoding": {
                "OrganisationId": {
                  "style": "form"
                },
                "ExcelFile": {
                  "style": "form"
                },
                "VerifyWithNin": {
                  "style": "form"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/onboard/upload": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Bulk-imports retirees from an Excel sheet.",
        "description": "Send `multipart/form-data` with `ExcelFile` (.xls or .xlsx, up to 5 MB) and, optionally, `OrganisationId`. It defaults to the caller's organisation; Admins with no organisation must send it. To see what will be imported first, send the same file to `POST /api/Admin/onboard/preview`.\r\n            \r\nThe data goes on the worksheet named `Sheet1` (or the first worksheet), one retiree per row; blank rows are skipped. If row 1 holds column names it's treated as a header row and skipped, and columns are matched by name, in any order:\r\n            \r\n- Required: `RetireeNumber` (pension ID), `LastName`, `FirstName`, `DateOfBirth` (dd/MM/yyyy).\r\n- Optional: `OtherName`, `Email`, `PhoneNumber`, `BVN`, `NIN`, `Address`, `NextOfKin`, `NextOfKinPhoneNumber`, `NextOfKinRelationship`, `SecondNextOfKin`, `SecondNextOfKinPhoneNumber`, `SecondNextOfKinRelationship`.\r\n            \r\nNames ignore case and spacing, and a few alternatives are accepted (for example `Pension ID`, `Surname`, `DOB`). Columns with other names are ignored. Without a header row, the columns are read in the order above, starting at row 1.\r\n            \r\n`BVN` and `NIN` are 11 digits and either or both can be left blank; blanks are saved as empty (NULL). A BVN or NIN can't belong to two pensioners in the organisation.\r\n            \r\nA retiree with only a NIN is set up to verify with their NIN, and one with a BVN with their BVN. With neither, no identity type is set; the retiree enters a BVN or NIN at their first verification. For retirees with both, send their pension IDs in `VerifyWithNin` (one form field each) to have them verify with their NIN instead. A pension ID sent there whose row has no NIN is a problem with that row.\r\n            \r\nThe whole file is rejected if any row has a problem, including a pension ID that's repeated in the file or already in the system. The message lists the problems by row number.\r\n            \r\nOn success `message` says how many records were imported.\r\n            \r\n**Errors (400):**\r\n- `-1` Missing file, wrong type or size, or problems in the sheet.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin or Manager.",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "OrganisationId": {
                    "type": "string"
                  },
                  "ExcelFile": {
                    "type": "string",
                    "format": "binary"
                  },
                  "VerifyWithNin": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Pension IDs of pensioners with both a BVN and a NIN who should verify with their NIN.\r\nThey verify with their BVN otherwise. Send one form field per pension ID."
                  }
                }
              },
              "encoding": {
                "OrganisationId": {
                  "style": "form"
                },
                "ExcelFile": {
                  "style": "form"
                },
                "VerifyWithNin": {
                  "style": "form"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Imported."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/audit-trail": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Records an action in the audit trail.",
        "description": "Send `actionType`, `details` and `result`. The entry is stored against the caller and their organisation with the current time. The admin portal calls this to log what its users do.\r\n            \r\n**Errors (400):**\r\n- `-l` The entry couldn't be saved.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.\n\n**Encrypted request:** shown here is the `AuditTrailRequestDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuditTrailRequestDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Recorded."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "AuditTrailRequestDto",
          "type": "object"
        }
      },
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "The caller's own audit-trail entries.",
        "description": "Paginated (`page` and `pageSize`, 20 by default), newest first, optionally between `fromDate` and `toDate`. Only the caller's own entries in their organisation: `userId` and `organisationId` are ignored.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin, Manager, Supervisor or Officer.",
        "parameters": [
          {
            "name": "UserId",
            "in": "query",
            "description": "Ignored: always the caller.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OrganisationId",
            "in": "query",
            "description": "Ignored: always the caller's organisation.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "FromDate",
            "in": "query",
            "description": "Earliest entry time to include.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "ToDate",
            "in": "query",
            "description": "Latest entry time to include.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "Page",
            "in": "query",
            "description": "Page number, from 1.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "PageSize",
            "in": "query",
            "description": "Entries per page.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of entries."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/user/add": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Creates a staff user.",
        "description": "Send the organisation, names, phone, email (also the username), password and role. The account starts active with a permanent password, so the user isn't asked to change it at first sign-in. The password must meet the ASP.NET Identity password rules.\r\n            \r\n**Errors (400):**\r\n- `-l` The user couldn't be created, for example because the email is taken or the password was rejected.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin.\n\n**Encrypted request:** shown here is the `UserRegisterDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserRegisterDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Created."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "UserRegisterDto",
          "type": "object"
        }
      }
    },
    "/api/Admin/users/list": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Lists all staff users, with the fields needed to edit them.",
        "description": "The same list as `GET /api/Admin/accounts`, plus first and last name, email, phone and organisation ID as separate fields. Covers all organisations.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin.",
        "responses": {
          "200": {
            "description": "The users."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/user/update": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Updates any staff user's name, contact details and role.",
        "description": "Finds the user by `id` and replaces their names, email and phone number. This is the only endpoint that changes another user or a role; `POST /api/Admin/account/update` only lets staff edit their own details.\r\n            \r\n- `role` must be an existing role (see `GET /api/Admin/roles`). Leave it out to keep the current one.\r\n- `organisationId` is ignored.\r\n            \r\n**Errors (400):**\r\n- `-l` User not found, unknown role, or the update failed. An unknown role is refused before anything is changed.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin.\n\n**Encrypted request:** shown here is the `UserEditDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserEditDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Updated."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "UserEditDto",
          "type": "object"
        }
      }
    },
    "/api/Admin/organisation/add": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Creates an organisation.",
        "description": "Send `name`, `code` and `contacts`. `code` must be unique, ignoring case. Retirees, staff users and verification periods all belong to an organisation.\r\n            \r\n**Errors (400):**\r\n- `-l` The code is already used, or the organisation couldn't be saved.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin.\n\n**Encrypted request:** shown here is the `OrganisationDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganisationDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Created."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "OrganisationDto",
          "type": "object"
        }
      }
    },
    "/api/Admin/organisation/update": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "🔒 Updates an organisation's name, code and contacts.",
        "description": "Finds the organisation by `id` and replaces `name`, `code` and `contacts`. `isActive` is ignored and the organisation is always left active. The new `code` isn't checked for uniqueness.\r\n            \r\n**Errors (400):**\r\n- `-l` Organisation not found (the message says \"User not found\"), or it couldn't be saved.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin.\n\n**Encrypted request:** shown here is the `OrganisationEditDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganisationEditDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Updated."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "OrganisationEditDto",
          "type": "object"
        }
      }
    },
    "/api/Admin/organisation/list": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Lists all organisations.",
        "description": "Every organisation, with its name, code, contacts, status and message units.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin.",
        "responses": {
          "200": {
            "description": "The organisations."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Admin/roles": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Lists the roles that can be given to staff users.",
        "description": "The role names, such as `Admin`, `Manager`, `Supervisor` and `Officer`.\n\n**Access:** Staff bearer token, from `POST /api/Auth/admin-verify-2fa`, with the role Admin.",
        "responses": {
          "200": {
            "description": "The role names."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Auth/admin-login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "🔒 Staff sign-in, step 1: checks the email and password and sends a two-factor code.",
        "description": "Checks the staff user's email and password. If they're right, generates an 8-character code and returns a `pendingToken`. The code is sent by email and SMS (whichever the user has on file) straight after the response, so the response doesn't wait for delivery. If it doesn't arrive, use `POST /api/Auth/admin-resend-2fa`.\r\n            \r\nNothing is signed in yet: send the `pendingToken` and the code to `POST /api/Auth/admin-verify-2fa` to get a bearer token.\r\n            \r\n- The `pendingToken` expires after 10 minutes.\r\n- The response is a bare object, not the usual envelope: `{ \"requiresTwoFactor\": true, \"pendingToken\": \"...\" }`.\r\n            \r\n**Errors (400):**\r\n- `-1` Wrong email or password.\r\n- `-2` The user has no email or phone number to send the code to.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `UserLoginDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserLoginDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Password accepted and code sent."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "UserLoginDto",
          "type": "object"
        }
      }
    },
    "/api/Auth/admin-resend-2fa": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "🔒 Staff sign-in: sends a new two-factor code for a pending login.",
        "description": "Replaces the code for a `pendingToken` from `POST /api/Auth/admin-login` and sends the new one by email and SMS. The previous code stops working.\r\n            \r\n- At most 3 resends per login, at least 60 seconds apart.\r\n- The `pendingToken` keeps its original 10-minute expiry.\r\n- Response: `{ \"resent\": true }`.\r\n            \r\n**Errors (400):**\r\n- `-1` The pending login has expired or doesn't exist. Sign in again.\r\n- `-3` Too soon; `message` says how many seconds to wait.\r\n- `-4` Resend limit reached. Sign in again.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `TwoFactorResendDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TwoFactorResendDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "New code sent."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "TwoFactorResendDto",
          "type": "object"
        }
      }
    },
    "/api/Auth/admin-verify-2fa": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "🔒 Staff sign-in, step 2: exchanges the two-factor code for a bearer token.",
        "description": "Checks `code` (not case-sensitive) against the code sent for `pendingToken`. If it matches, returns a staff bearer token valid for 15 minutes. The response is a bare object:\r\n            \r\n- `{ \"changePassword\": false, \"id\": \"...\", \"token\": \"...\", \"role\": \"Admin\" }`\r\n- `{ \"changePassword\": true, \"id\": \"...\", \"token\": \"...\" }` when the user still has a temporary password. Call `POST /api/Auth/change-password` before anything else.\r\n            \r\nThe token carries the user's ID, roles and organisation, and admin endpoints only show that organisation's data. Signing in ends the user's previous session, so their older tokens get 401.\r\n            \r\n**Errors (400):**\r\n- `-1` Wrong code, or the pending login has expired.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `TwoFactorVerifyDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TwoFactorVerifyDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Signed in."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "TwoFactorVerifyDto",
          "type": "object"
        }
      }
    },
    "/api/Auth/forgot-password": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "🔒 Password reset, step 1: emails a reset code to a staff user.",
        "description": "Emails a 6-character reset code to the staff user with this email address. The code is valid for 10 minutes.\r\n            \r\nResponse: `{ \"status\": true, \"message\": \"...\" }`.\r\n            \r\n**Errors (400):**\r\n- `-1` No staff user has that email address.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `ForgotPasswordRequestDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ForgotPasswordRequestDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Code sent."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "ForgotPasswordRequestDto",
          "type": "object"
        }
      }
    },
    "/api/Auth/forgot-password-otp": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "🔒 Password reset, step 2: checks the emailed reset code.",
        "description": "Confirms `otp` is the code emailed for `email` and that it's less than 10 minutes old. Changes nothing; use it to move the user on to choosing a new password.\r\n            \r\nResponse: `{ \"status\": true, \"message\": \"...\" }`.\r\n            \r\n**Errors (400):**\r\n- `-1` Wrong or expired code.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `ForgotPasswordOTPConfirmationDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ForgotPasswordOTPConfirmationDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Code is valid."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "ForgotPasswordOTPConfirmationDto",
          "type": "object"
        }
      }
    },
    "/api/Auth/forgot-password-new-password": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "🔒 Password reset, step 3: sets the new password.",
        "description": "Sets `newPassword` when `otp` matches the emailed code. Accepted until 15 minutes after the code was sent, 5 minutes longer than step 2 allows. The password must meet the ASP.NET Identity password rules.\r\n            \r\nResponse: `{ \"status\": true, \"message\": \"...\" }`.\r\n            \r\n**Errors (400):**\r\n- `-1` Wrong or expired code, or the password was rejected.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `ForgotPasswordNewPasswordDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ForgotPasswordNewPasswordDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Password changed."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "ForgotPasswordNewPasswordDto",
          "type": "object"
        }
      }
    },
    "/api/Auth/change-password": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "🔒 Changes the signed-in staff user's password.",
        "description": "Changes the password of the user in the bearer token, given their `currentPassword`. Also the next step after sign-in when `changePassword` is true.\r\n            \r\nSend the staff token even though the API doesn't enforce one: the user is read from it, and without a token the call fails with 500.\r\n            \r\nResponse: `{ \"status\": true, \"message\": \"...\" }`.\r\n            \r\n**Errors (400):**\r\n- `-1` Current password is wrong, or the new one was rejected.\n\n**Access:** No token enforced by the API.\n\n**Encrypted request:** shown here is the `ChangePasswordDto` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePasswordDto"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Password changed."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "ChangePasswordDto",
          "type": "object"
        }
      }
    },
    "/api/Auth/token": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Legacy: issues a long-lived token for an API user account.",
        "description": "Checks the credentials against the separate API-user table (not staff accounts) and returns `{ \"token\": \"...\" }`, valid for 365 days and carrying that API user's roles. `username` must be sent in lowercase.\r\n            \r\n**Known issue:** these tokens have no user ID, so the session check fails on every protected endpoint with 500. Nothing in this API can currently be called with them.\r\n            \r\n**Errors (400):**\r\n- `-1` Wrong username or password.\n\n**Access:** No token enforced by the API.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "$ref": "#/components/schemas/TokenModel"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenModel"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenModel"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/TokenModel"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          }
        }
      }
    },
    "/api/Auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Revokes the bearer token sent with the request.",
        "description": "Adds the token in the `Authorization` header to the revocation list for 30 hours; requests using it then get 401. Returns plain text, not JSON.\r\n            \r\nReturns 400 when there's no `Authorization` header.\n\n**Access:** No token enforced by the API.",
        "responses": {
          "200": {
            "description": "Token revoked."
          },
          "400": {
            "description": "No `Authorization` header."
          }
        }
      }
    },
    "/api/Auth/encrypt": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Legacy helper: encrypts a string with the server's payload key.",
        "description": "Takes a JSON string, such as `\"PEN100000\"` or an escaped JSON object, and returns the base64 ciphertext of its contents as a JSON string, ready to send as `payload`. Available in every environment. The /docs page's encryption tools do the same for testing.\n\n**Access:** Anonymous: no token needed.",
        "requestBody": {
          "description": "The text to encrypt, as a JSON string.",
          "content": {
            "application/json-patch+json": {
              "schema": {
                "type": "string"
              }
            },
            "application/json": {
              "schema": {
                "type": "string"
              }
            },
            "text/json": {
              "schema": {
                "type": "string"
              }
            },
            "application/*+json": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ciphertext."
          }
        }
      }
    },
    "/api/Retirees/requestOtp": {
      "post": {
        "tags": [
          "Retirees"
        ],
        "summary": "🔒 Retiree login, step 1: sends a one-time code for a pension ID.",
        "description": "The payload is the pension ID as a JSON string, for example `\"PEN100000\"`.\r\n            \r\nSends an 8-character code by SMS and, when the retiree has an email address, by email. The code is valid for 5 minutes. It's only sent when the retiree's organisation has an active verification period and the retiree hasn't already been verified in it (unless an admin has allowed a retry).\r\n            \r\nOn success `responseCode` is `\"0\"`; `data` holds nothing the client needs.\r\n            \r\n**Errors (400):**\r\n- `-5` No active verification period, or already verified in the current one; `message` gives the period's dates.\r\n- `-6` Pension ID not found.\r\n- `-7` The code couldn't be sent.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `String` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "string"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Code sent.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyOTPModelResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyOTPModelResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyOTPModelResponseModel"
                }
              }
            }
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              }
            }
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "String",
          "type": "string"
        }
      }
    },
    "/api/Retirees/VerifyOtp": {
      "post": {
        "tags": [
          "Retirees"
        ],
        "summary": "🔒 Retiree login, step 2: exchanges the one-time code for a bearer token.",
        "description": "Payload: `{ \"otp\": \"...\", \"retireeNumber\": \"...\" }`.\r\n            \r\nOn success `data` contains:\r\n- `authToken`: the retiree's bearer token, valid for 30 minutes. Signing in again ends the previous session, so older tokens get 401.\r\n- `retiree`: name, phone, email, date of birth, `identityType` (null when the retiree has no BVN or NIN yet), `isReturningUser` (true when a BVN or NIN is on record, so the client shouldn't ask for one), and `hasBvn`/`hasNin` (which IDs are on record; with both, let the retiree choose which to verify with). BVN, NIN, address and next of kin are never returned here; use `GET /api/Retirees/contact-details` for the address and next of kin.\r\n            \r\nEach code works once. Three wrong codes lock the account for 30 minutes.\r\n            \r\n**Errors (400):**\r\n- `-6` Pension ID not found.\r\n- `-7` Wrong or expired code, or the account is locked; `message` says for how long.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `SmsRec` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmsRec"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Signed in.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              }
            }
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              }
            }
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "SmsRec",
          "type": "object"
        }
      }
    },
    "/api/Retirees/Validate": {
      "post": {
        "tags": [
          "Retirees"
        ],
        "summary": "🔒 Liveness check: matches the retiree's selfie against their BVN or NIN photo.",
        "description": "The retiree is taken from the token; `retireeNumber` in the payload is ignored.\r\n            \r\n- First-time retirees (`isReturningUser` false) send `type` with the matching `bvn` or `nin` (11 digits; spaces are ignored). If no other retiree in the organisation has it, it's saved to their record.\r\n- Returning retirees send only `type` and `imageData`, and the ID on record for that type is used. Send their `identityType`, or, when `hasBvn` and `hasNin` are both true, whichever the retiree chose. A `type` they have no ID for falls back to the one they have.\r\n            \r\nThe ID is looked up with the BVN/NIN provider, and the provider's photo is compared with `imageData`.\r\n            \r\nWhen `responseCode` is `\"0\"`, the check ran:\r\n- Face matched: recorded as a successful verification. `data.referenceNo` is the verification number, which is also sent to the retiree by SMS and email.\r\n- Face didn't match: recorded as a failed verification, and `data.referenceNo` is `0`. The message still says \"Validation successful\", so check `referenceNo`.\r\n            \r\nOnly one verification per retiree counts in each verification period.\r\n            \r\n**Errors (400):**\r\n- `-1` Pension ID not found.\r\n- `-2` That BVN or NIN belongs to another pensioner.\r\n- `-3` The BVN or NIN doesn't match the one on record, or a first-time BVN or NIN isn't 11 digits (including when none was sent).\r\n- `-12` No active verification period, or already verified in this one.\r\n- `-13` The ID provider couldn't be reached or didn't find the ID. Try again later.\n\n**Access:** Any valid bearer token, staff or retiree.\n\n**Encrypted request:** shown here is the `ValidationModel` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidationModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Check completed; see `data.referenceNo`.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              }
            }
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              }
            }
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "ValidationModel",
          "type": "object"
        }
      }
    },
    "/api/Retirees/Update": {
      "post": {
        "tags": [
          "Retirees"
        ],
        "summary": "🔒 Saves the retiree's address and next-of-kin details.",
        "description": "With a retiree token, the retiree in the token is updated and `retireeId` is ignored. Staff tokens must send `retireeId`.\r\n            \r\n- Blank or missing fields keep the value on record, so you can send only what changed. Fields can't be cleared.\r\n- `bvn` is only saved for retirees with no BVN or NIN on record, and is rejected if another retiree in the organisation has it.\r\n            \r\n`data.retiree` is the trimmed retiree summary; BVN, NIN, address and next of kin are not echoed back.\r\n            \r\n**Errors (400):**\r\n- `-2` That BVN belongs to another pensioner.\r\n- `-3` Retiree not found.\n\n**Access:** Any valid bearer token, staff or retiree.\n\n**Encrypted request:** shown here is the `UpdateRetireeModel` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateRetireeModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeResultModelResponseModel"
                }
              }
            }
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              }
            }
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "UpdateRetireeModel",
          "type": "object"
        }
      }
    },
    "/api/Retirees/contact-details": {
      "get": {
        "tags": [
          "Retirees"
        ],
        "summary": "The signed-in retiree's address and next of kin, to pre-fill the review screen.",
        "description": "Returns `address`, `nextOfKin`, `nokPhoneNumber`, `nokRelationship`, `secondNextOfKinName`, `secondNextOfKinPhoneNumber` and `secondNextOfKinRelationship` for the retiree in the token.\r\n            \r\nThere's no ID parameter, so retirees can only read their own details. Never includes BVN or NIN. Sent with `Cache-Control: no-store` so browsers and proxies don't keep a copy.\n\n**Access:** Retiree bearer token, from `POST /api/Retirees/VerifyOtp`.",
        "responses": {
          "200": {
            "description": "The contact details.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeContactDetailsModelResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeContactDetailsModelResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetireeContactDetailsModelResponseModel"
                }
              }
            }
          },
          "404": {
            "description": "The retiree in the token no longer exists.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/StringResponseModel"
                }
              }
            }
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          },
          "403": {
            "description": "The token doesn't have a required role."
          }
        }
      }
    },
    "/api/Retirees/offline": {
      "post": {
        "tags": [
          "Retirees"
        ],
        "summary": "Batch liveness checks for verifications captured offline.",
        "description": "Takes a plain JSON array of `ValidationModel` (not encrypted). Each item carries its own `retireeNumber`, `type`, `bvn` or `nin`, and `imageData`, and goes through the same check as `POST /api/Retirees/Validate`, one after another.\r\n            \r\n`data` has one entry per item that got far enough to load the retiree: `{ \"retireeNumber\", \"reference\", \"code\", \"message\" }`, where `code` and `reference` mean the same as `Validate`'s `responseCode` and `referenceNo`. Items that fail with `-1`, `-2`, `-3` or `-13` are left out, so compare the result with what you sent.\n\n**Access:** Any valid bearer token, staff or retiree.",
        "requestBody": {
          "description": "The captured verifications.",
          "content": {
            "text/csv": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ValidationModel"
                }
              }
            },
            "application/json-patch+json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ValidationModel"
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ValidationModel"
                }
              }
            },
            "text/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ValidationModel"
                }
              }
            },
            "application/*+json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ValidationModel"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Processed."
          },
          "400": {
            "description": "Failed; see `responseCode` and `message`."
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          }
        }
      }
    },
    "/api/Retirees/Confirm": {
      "post": {
        "tags": [
          "Retirees"
        ],
        "summary": "🔒 Checks that a verification reference number exists.",
        "description": "The payload is the reference number as a JSON number, for example `12345`. `data` is `true` when a verification with that number exists. Doesn't check who it belongs to or whether it succeeded.\n\n**Access:** Any valid bearer token, staff or retiree.\n\n**Encrypted request:** shown here is the `Int32` inside the payload. On the wire, the body is `{ \"payload\": \"<ciphertext>\" }`, where the ciphertext is this model's JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "integer",
                "format": "int32"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "It exists.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              }
            }
          },
          "400": {
            "description": "Not found, or the payload couldn't be read.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/BooleanResponseModel"
                }
              }
            }
          },
          "401": {
            "description": "No token, or it has expired, been revoked, or been replaced by a newer login."
          }
        },
        "x-encrypted-payload": {
          "in": "body",
          "name": "payload",
          "model": "Int32",
          "type": "integer"
        }
      }
    },
    "/api/Retirees/Image": {
      "get": {
        "tags": [
          "Retirees"
        ],
        "summary": "🔒 Returns a stored image as base64.",
        "description": "`payload` is the encrypted path of the image under the API's `wwwroot`, as a JSON string, for example `\"Image/Success/App/{file}.png\"`. Returns `{ \"image\": \"{base64}\" }`, a bare object rather than the usual envelope.\n\n**Access:** Anonymous: no token needed.\n\n**Encrypted request:** shown here is the `String` inside the payload. On the wire, the `payload` query parameter carries the ciphertext of this value as JSON, AES-CBC encrypted with the server's key and base64 encoded. The docs page (/docs) encrypts *Try it out* requests for you.",
        "parameters": [
          {
            "name": "payload",
            "in": "query",
            "description": "Encrypted image path.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image."
          },
          "404": {
            "description": "No such file."
          },
          "500": {
            "description": "The payload couldn't be read."
          }
        },
        "x-encrypted-payload": {
          "in": "query",
          "name": "payload",
          "model": "String",
          "type": "string"
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AuditTrailRequestDto": {
        "type": "object",
        "properties": {
          "actionType": {
            "type": "string",
            "nullable": true
          },
          "details": {
            "type": "string",
            "nullable": true
          },
          "result": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "BooleanResponseModel": {
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "responseCode": {
            "type": "string",
            "nullable": true
          },
          "data": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "ChangePasswordDto": {
        "type": "object",
        "properties": {
          "currentPassword": {
            "type": "string",
            "nullable": true
          },
          "newPassword": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ForgotPasswordNewPasswordDto": {
        "required": [
          "email"
        ],
        "type": "object",
        "properties": {
          "email": {
            "minLength": 1,
            "type": "string",
            "format": "email"
          },
          "otp": {
            "type": "string",
            "nullable": true
          },
          "newPassword": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ForgotPasswordOTPConfirmationDto": {
        "required": [
          "email"
        ],
        "type": "object",
        "properties": {
          "email": {
            "minLength": 1,
            "type": "string",
            "format": "email"
          },
          "otp": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ForgotPasswordRequestDto": {
        "required": [
          "email"
        ],
        "type": "object",
        "properties": {
          "email": {
            "minLength": 1,
            "type": "string",
            "format": "email"
          }
        },
        "additionalProperties": false
      },
      "MessageDto": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "messageType": {
            "$ref": "#/components/schemas/MessageType"
          },
          "sendTo": {
            "$ref": "#/components/schemas/SendTo"
          },
          "file": {
            "type": "string",
            "format": "binary",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "MessageType": {
        "enum": [
          "Email",
          "Sms"
        ],
        "type": "string"
      },
      "OrganisationDto": {
        "required": [
          "code",
          "contacts",
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "code": {
            "minLength": 1,
            "type": "string"
          },
          "contacts": {
            "minLength": 1,
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "OrganisationEditDto": {
        "required": [
          "code",
          "contacts",
          "id",
          "isActive",
          "name"
        ],
        "type": "object",
        "properties": {
          "id": {
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "code": {
            "minLength": 1,
            "type": "string"
          },
          "contacts": {
            "minLength": 1,
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "PayloadModel": {
        "type": "object",
        "properties": {
          "payload": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "RetireeContactDetailsModel": {
        "type": "object",
        "properties": {
          "address": {
            "type": "string",
            "nullable": true
          },
          "nextOfKin": {
            "type": "string",
            "nullable": true
          },
          "nokPhoneNumber": {
            "type": "string",
            "nullable": true
          },
          "nokRelationship": {
            "type": "string",
            "nullable": true
          },
          "secondNextOfKinName": {
            "type": "string",
            "nullable": true
          },
          "secondNextOfKinPhoneNumber": {
            "type": "string",
            "nullable": true
          },
          "secondNextOfKinRelationship": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The contact details a retiree reviews on the \"Verify your details\" screen.\r\nOnly ever returned to the retiree themselves (GET api/Retirees/contact-details)."
      },
      "RetireeContactDetailsModelResponseModel": {
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "responseCode": {
            "type": "string",
            "nullable": true
          },
          "data": {
            "$ref": "#/components/schemas/RetireeContactDetailsModel"
          }
        },
        "additionalProperties": false
      },
      "RetireeResultModel": {
        "type": "object",
        "properties": {
          "retiree": {
            "$ref": "#/components/schemas/RetireeSummaryModel"
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "authToken": {
            "type": "string",
            "description": "Bearer token for the retiree endpoints. Only set by VerifyOtp; valid for 30 minutes.",
            "nullable": true
          },
          "referenceNo": {
            "type": "integer",
            "description": "Verification reference number. Only set by Validate.",
            "format": "int32"
          },
          "errorCode": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "What VerifyOtp, Validate and Update return to clients: AmAliveApi.Core.Models.ResultModel's shape,\r\nwith the retiree trimmed to AmAliveApi.Core.Models.RetireeSummaryModel."
      },
      "RetireeResultModelResponseModel": {
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "responseCode": {
            "type": "string",
            "nullable": true
          },
          "data": {
            "$ref": "#/components/schemas/RetireeResultModel"
          }
        },
        "additionalProperties": false
      },
      "RetireeSummaryModel": {
        "type": "object",
        "properties": {
          "retireeNumber": {
            "type": "string",
            "nullable": true
          },
          "firstName": {
            "type": "string",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "otherName": {
            "type": "string",
            "nullable": true
          },
          "phoneNumber": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "dateOfBirth": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "identityType": {
            "type": "string",
            "description": "\"BVN\" or \"NIN\": the ID a returning retiree is verified against by default. Null for a retiree\r\nonboarded with neither.",
            "nullable": true
          },
          "isReturningUser": {
            "type": "boolean",
            "description": "True when a BVN or NIN is already on record, so the client doesn't ask for one."
          },
          "hasBvn": {
            "type": "boolean",
            "description": "A BVN is on record. The BVN itself is never returned."
          },
          "hasNin": {
            "type": "boolean",
            "description": "A NIN is on record. The NIN itself is never returned. With both, the retiree can choose which\r\none to verify with."
          }
        },
        "additionalProperties": false,
        "description": "Trimmed retiree projection returned to clients. Excludes BVN, NIN, address,\r\nnext of kin and internal audit fields to avoid leaking PII (VAPT finding #1)."
      },
      "SendTo": {
        "enum": [
          "AllRetirees",
          "UnverifiedRetirees",
          "SelectedRetirees"
        ],
        "type": "string"
      },
      "SmsRec": {
        "type": "object",
        "properties": {
          "otp": {
            "type": "string",
            "nullable": true
          },
          "retireeNumber": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "StringResponseModel": {
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "responseCode": {
            "type": "string",
            "nullable": true
          },
          "data": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "TokenModel": {
        "required": [
          "password",
          "username"
        ],
        "type": "object",
        "properties": {
          "username": {
            "minLength": 1,
            "type": "string"
          },
          "password": {
            "minLength": 1,
            "type": "string"
          },
          "isReturning": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "TwoFactorResendDto": {
        "required": [
          "pendingToken"
        ],
        "type": "object",
        "properties": {
          "pendingToken": {
            "minLength": 1,
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "TwoFactorVerifyDto": {
        "required": [
          "code",
          "pendingToken"
        ],
        "type": "object",
        "properties": {
          "pendingToken": {
            "minLength": 1,
            "type": "string"
          },
          "code": {
            "minLength": 1,
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "UpdateRetireeModel": {
        "type": "object",
        "properties": {
          "retireeId": {
            "type": "integer",
            "description": "Ignored for retiree tokens (the retiree comes from the token). Required for staff tokens.",
            "format": "int32"
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "bvn": {
            "type": "string",
            "description": "Only stored for first-time retirees (no BVN/NIN on record), and rejected if another retiree in the organisation has it.",
            "nullable": true
          },
          "nextOfKin": {
            "type": "string",
            "nullable": true
          },
          "nokPhoneNumber": {
            "type": "string",
            "nullable": true
          },
          "nokRelationship": {
            "type": "string",
            "nullable": true
          },
          "secondNextOfKinName": {
            "type": "string",
            "nullable": true
          },
          "secondNextOfKinPhoneNumber": {
            "type": "string",
            "nullable": true
          },
          "secondNextOfKinRelationship": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Address and next-of-kin changes. Blank fields keep the value already on record."
      },
      "UpdatedIDStatusDto": {
        "type": "object",
        "properties": {
          "retireeNo": {
            "type": "string",
            "nullable": true
          },
          "identityNumber": {
            "type": "string",
            "nullable": true
          },
          "identityType": {
            "type": "string",
            "nullable": true
          },
          "updateStatus": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false
      },
      "UserEditDto": {
        "required": [
          "email",
          "firstName",
          "lastName",
          "organisationId",
          "role"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "organisationId": {
            "minLength": 1,
            "type": "string"
          },
          "firstName": {
            "minLength": 1,
            "type": "string"
          },
          "lastName": {
            "minLength": 1,
            "type": "string"
          },
          "phoneNumber": {
            "maxLength": 13,
            "type": "string",
            "nullable": true
          },
          "email": {
            "minLength": 1,
            "type": "string",
            "format": "email"
          },
          "role": {
            "minLength": 1,
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "UserLoginDto": {
        "required": [
          "email",
          "password"
        ],
        "type": "object",
        "properties": {
          "email": {
            "minLength": 1,
            "type": "string",
            "format": "email"
          },
          "password": {
            "minLength": 1,
            "type": "string"
          },
          "rememberMe": {
            "type": "boolean"
          },
          "returnUrl": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "UserRegisterDto": {
        "required": [
          "email",
          "firstName",
          "lastName",
          "organisationId",
          "password",
          "role"
        ],
        "type": "object",
        "properties": {
          "organisationId": {
            "minLength": 1,
            "type": "string"
          },
          "firstName": {
            "minLength": 1,
            "type": "string"
          },
          "lastName": {
            "minLength": 1,
            "type": "string"
          },
          "phoneNumber": {
            "maxLength": 13,
            "type": "string",
            "nullable": true
          },
          "email": {
            "minLength": 1,
            "type": "string",
            "format": "email"
          },
          "password": {
            "minLength": 1,
            "type": "string"
          },
          "role": {
            "minLength": 1,
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "ValidationModel": {
        "type": "object",
        "properties": {
          "retireeNumber": {
            "type": "string",
            "description": "Validate ignores this and uses the retiree in the token. Required by the offline batch endpoint.",
            "nullable": true
          },
          "bvn": {
            "type": "string",
            "description": "Required for first-time retirees verifying by BVN (11 digits).",
            "nullable": true
          },
          "nin": {
            "type": "string",
            "description": "Required for first-time retirees verifying by NIN (11 digits).",
            "nullable": true
          },
          "imageData": {
            "type": "string",
            "description": "Selfie as base64, without the \"data:image/...;base64,\" prefix.",
            "nullable": true
          },
          "type": {
            "type": "string",
            "description": "\"BVN\" or \"NIN\".",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Liveness check. Returning retirees can leave BVN and NIN empty: the ID on record is used."
      },
      "VerificationPeriod": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "organisationId": {
            "type": "string",
            "nullable": true
          },
          "fromDate": {
            "type": "string",
            "format": "date-time"
          },
          "toDate": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "VerifyOTPModel": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "nullable": true
          },
          "authToken": {
            "type": "string",
            "nullable": true
          },
          "referenceNo": {
            "type": "integer",
            "format": "int32"
          },
          "errorCode": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "VerifyOTPModelResponseModel": {
        "type": "object",
        "properties": {
          "isError": {
            "type": "boolean"
          },
          "message": {
            "type": "string",
            "nullable": true
          },
          "responseCode": {
            "type": "string",
            "nullable": true
          },
          "data": {
            "$ref": "#/components/schemas/VerifyOTPModel"
          }
        },
        "additionalProperties": false
      }
    },
    "securitySchemes": {
      "Bearer": {
        "type": "apiKey",
        "description": "JWT Authorization header using the Bearer scheme. \\r\\n\\r\\n\r\n                      Enter 'Bearer' [space] and then your token in the text input below.\r\n                      \\r\\n\\r\\nExample: 'Bearer 12345abcdef'",
        "name": "Authorization",
        "in": "header"
      }
    }
  },
  "security": [
    {
      "Bearer": [ ]
    }
  ],
  "tags": [
    {
      "name": "Admin",
      "description": "Admin portal: dashboards, verification operations, retiree records, notifications, onboarding, the audit trail, staff users and organisations. Staff tokens only."
    },
    {
      "name": "Auth",
      "description": "Staff sign-in (password, then a two-factor code), password recovery, and legacy helpers."
    },
    {
      "name": "Retirees",
      "description": "Pensioner (retiree) verification: OTP login, contact details and the liveness check. Used by the web and mobile apps."
    }
  ]
}