{
  "openapi": "3.1.0",
  "info": {
    "title": "Hubtoll API",
    "version": "v1",
    "summary": "Read your company's Hubtoll data from your own systems.",
    "description": "OAuth 2.0 client-credentials REST API. Every endpoint is scoped to the company that owns the credential — there is no company identifier in any URL, so one company's integration cannot reach another's data. All endpoints are reads.",
    "contact": { "name": "Hubtoll support", "email": "cloud@digitalvortextech.org", "url": "https://hubtoll.org/developers/" }
  },
  "servers": [
    { "url": "https://api.hubtoll.com/api", "description": "Production" }
  ],
  "security": [ { "hubtollOAuth": [] } ],
  "tags": [
    { "name": "Sales &amp; receivables", "description": "The money-in side: who you sell to, what you sell, and every document from quote to settled invoice." },
    { "name": "Purchasing &amp; payables", "description": "The money-out side: suppliers, requisitions, purchase orders, bills and what you have paid." },
    { "name": "Inventory", "description": "Stock movements — what came in against a purchase order, and every correction since." },
    { "name": "The books", "description": "Your double-entry ledger and the statements derived from it. Nothing here is a stored figure; it is all computed from the journals." },
    { "name": "People", "description": "Your staff directory plus company reference data — where you operate and which days you are closed." },
    { "name": "Leave", "description": "Leave requests across the company and the categories they are booked against." },
    { "name": "CRM", "description": "Your pipeline: the companies and people you sell to, the deals in flight, and inbound leads." },
    { "name": "Identity", "description": "Prove a credential works and see what it can reach." }
  ],
  "paths": {
    "/v1/me": {
      "get": {
        "operationId": "me",
        "summary": "Who am I?",
        "description": "Confirms your token works and shows which company it belongs to and what it may access. Answers for any live credential, whatever its scopes — the fastest way to check a new one.",
        "tags": ["Identity"],
        "parameters": [
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"corporateId\": \"be183892-…\",\n  \"clientId\": \"htc_TyXk0mRB…\",\n  \"scopes\": [\"sales:read\", \"accounting:read\"]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/customers": {
      "get": {
        "operationId": "customers",
        "summary": "List customers",
        "description": "Your customer records. Paginated.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text match on name or email.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [\n    {\n      \"id\": \"4194c3c7-…\",\n      \"name\": \"Frames and heights\",\n      \"email\": \"accounts@framesandheights.com\",\n      \"currencyCode\": \"NGN\",\n      \"billingCurrencies\": [\"NGN\"]\n    }\n  ],\n  \"totalElements\": 5\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/customers/{id}": {
      "get": {
        "operationId": "customer",
        "summary": "Get one customer",
        "description": "A single customer. 404 if it does not belong to your company.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"4194c3c7-…\",\n  \"name\": \"Frames and heights\",\n  \"email\": \"accounts@framesandheights.com\",\n  \"currencyCode\": \"NGN\"\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/items": {
      "get": {
        "operationId": "items",
        "summary": "List items",
        "description": "The products and services you sell or buy, with their prices.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text match on the item name or SKU.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [\n    {\n      \"id\": \"9d2f…\",\n      \"name\": \"Consulting day\",\n      \"salesPrice\": \"150000.00\",\n      \"currencyCode\": \"NGN\"\n    }\n  ],\n  \"totalElements\": 5\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/items/{id}": {
      "get": {
        "operationId": "item",
        "summary": "Get one item",
        "description": "A single item, including its per-currency stated prices where it has any.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"9d2f…\",\n  \"name\": \"Consulting day\",\n  \"sku\": \"CONS-DAY\",\n  \"type\": \"SERVICE\",\n  \"salesPrice\": \"150000.00\",\n  \"currencyCode\": \"NGN\",\n  \"prices\": [ { \"currencyCode\": \"USD\", \"salesPrice\": \"900.00\" } ],\n  \"active\": true\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/quotes": {
      "get": {
        "operationId": "quotes",
        "summary": "List quotes",
        "description": "Quotes you have raised, with their status and whether they have been converted.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/quotes/{id}": {
      "get": {
        "operationId": "quote",
        "summary": "Get one quote",
        "description": "A single quote including its line items.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"QT-000004\",\n  \"customerName\": \"Frames and heights\",\n  \"date\": \"2026-08-01\",\n  \"expiryDate\": \"2026-08-31\",\n  \"status\": \"SENT\",\n  \"currencyCode\": \"NGN\",\n  \"subtotal\": \"450000.00\",\n  \"taxTotal\": \"0.00\",\n  \"total\": \"450000.00\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/sales-orders": {
      "get": {
        "operationId": "sales-orders",
        "summary": "List sales orders",
        "description": "Confirmed orders awaiting invoicing or delivery.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/sales-orders/{id}": {
      "get": {
        "operationId": "sales-order",
        "summary": "Get one sales order",
        "description": "A single order including its line items.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"SO-000002\",\n  \"customerName\": \"Frames and heights\",\n  \"date\": \"2026-08-02\",\n  \"expectedDate\": \"2026-08-20\",\n  \"status\": \"CONFIRMED\",\n  \"currencyCode\": \"NGN\",\n  \"total\": \"450000.00\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/invoices": {
      "get": {
        "operationId": "invoices",
        "summary": "List invoices",
        "description": "Sales invoices with their status and balance. The endpoint most integrations start with.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "DRAFT, PENDING_APPROVAL, ISSUED, PARTIALLY_PAID, PAID or VOID. An unknown value is a 400 naming the valid ones.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [\n    {\n      \"id\": \"575c2856-…\",\n      \"number\": \"INV-000012\",\n      \"customerName\": \"Frames and heights\",\n      \"date\": \"2026-08-06\",\n      \"dueDate\": \"2026-10-10\",\n      \"status\": \"ISSUED\",\n      \"currencyCode\": \"NGN\",\n      \"total\": \"450000.00\",\n      \"balanceDue\": \"450000.00\"\n    }\n  ],\n  \"totalElements\": 5\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/invoices/{id}": {
      "get": {
        "operationId": "invoice",
        "summary": "Get one invoice",
        "description": "A single invoice including its line items and its payment history.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"575c2856-…\",\n  \"number\": \"INV-000012\",\n  \"status\": \"ISSUED\",\n  \"currencyCode\": \"NGN\",\n  \"total\": \"450000.00\",\n  \"balanceDue\": \"450000.00\",\n  \"lines\": [\n    { \"itemName\": \"Consulting day\", \"qty\": \"3.000000\", \"rate\": \"150000.00\", \"lineNet\": \"450000.00\", \"lineTax\": \"0.00\" }\n  ],\n  \"payments\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/payments-received": {
      "get": {
        "operationId": "payments-received",
        "summary": "List payments received",
        "description": "Money in from customers, and which invoices each payment settled.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/payments-received/{id}": {
      "get": {
        "operationId": "payment-received",
        "summary": "Get one payment received",
        "description": "A single payment, its allocations against invoices (appliedAmount per invoice), any tax withheld at source (whtAmount), and a signed link to the branded receipt.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"PAY-000002\",\n  \"amount\": \"150000.00\",\n  \"whtAmount\": \"0.00\",\n  \"unappliedAmount\": \"0.00\",\n  \"receiptUrl\": \"https://…\",\n  \"applications\": [\n    { \"invoiceNumber\": \"INV-000012\", \"appliedAmount\": \"150000.00\" }\n  ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/credit-notes": {
      "get": {
        "operationId": "credit-notes",
        "summary": "List credit notes",
        "description": "Credits raised against customers.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/credit-notes/{id}": {
      "get": {
        "operationId": "credit-note",
        "summary": "Get one credit note",
        "description": "A single credit note, its lines and what it has been applied to.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"CN-000001\",\n  \"customerName\": \"Frames and heights\",\n  \"date\": \"2026-08-05\",\n  \"status\": \"ISSUED\",\n  \"currencyCode\": \"NGN\",\n  \"total\": \"50000.00\",\n  \"appliedAmount\": \"50000.00\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/sales-receipts": {
      "get": {
        "operationId": "sales-receipts",
        "summary": "List sales receipts",
        "description": "Cash sales — paid at the point of sale, with no invoice stage.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/sales-receipts/{id}": {
      "get": {
        "operationId": "sales-receipt",
        "summary": "Get one sales receipt",
        "description": "A single cash sale including its lines.",
        "tags": ["Sales &amp; receivables"],
        "security": [ { "hubtollOAuth": ["sales:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"RCPT-000003\",\n  \"customerName\": \"Walk-in customer\",\n  \"date\": \"2026-08-06\",\n  \"method\": \"CASH\",\n  \"bankAccountName\": \"Undeposited Funds\",\n  \"currencyCode\": \"NGN\",\n  \"total\": \"25000.00\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/vendors": {
      "get": {
        "operationId": "vendors",
        "summary": "List vendors",
        "description": "Your suppliers.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text match on name or email.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/vendors/{id}": {
      "get": {
        "operationId": "vendor",
        "summary": "Get one vendor",
        "description": "A single vendor, including the currencies they transact in.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"name\": \"Office Supplies Ltd\",\n  \"email\": \"accounts@supplies.example\",\n  \"currencyCode\": \"NGN\",\n  \"currencies\": [\"NGN\", \"USD\"],\n  \"paymentTerms\": 30,\n  \"whtApplicable\": true,\n  \"taxTreatment\": \"STANDARD\",\n  \"active\": true\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/purchase-requisitions": {
      "get": {
        "operationId": "purchase-requisitions",
        "summary": "List purchase requisitions",
        "description": "Internal requests to buy, with their approval status.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/purchase-requisitions/{id}": {
      "get": {
        "operationId": "purchase-requisition",
        "summary": "Get one requisition",
        "description": "A single requisition, its lines and its approval trail.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"REQ-000003\",\n  \"requestedByName\": \"Kofi Mensah\",\n  \"department\": \"Operations\",\n  \"date\": \"2026-08-01\",\n  \"status\": \"APPROVED\",\n  \"estimatedTotal\": \"120000.00\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/purchase-orders": {
      "get": {
        "operationId": "purchase-orders",
        "summary": "List purchase orders",
        "description": "Orders placed with suppliers.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/purchase-orders/{id}": {
      "get": {
        "operationId": "purchase-order",
        "summary": "Get one purchase order",
        "description": "A single PO including its lines.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"PO-000002\",\n  \"vendorName\": \"Office Supplies Ltd\",\n  \"date\": \"2026-08-03\",\n  \"expectedDate\": \"2026-08-17\",\n  \"status\": \"ISSUED\",\n  \"currencyCode\": \"NGN\",\n  \"committedTotal\": \"120000.00\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/bills": {
      "get": {
        "operationId": "bills",
        "summary": "List bills",
        "description": "Supplier bills with their status and outstanding balance.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/bills/{id}": {
      "get": {
        "operationId": "bill",
        "summary": "Get one bill",
        "description": "A single bill, its lines and its approval trail.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"BILL-000004\",\n  \"vendorName\": \"Office Supplies Ltd\",\n  \"date\": \"2026-08-01\",\n  \"dueDate\": \"2026-08-31\",\n  \"status\": \"POSTED\",\n  \"currencyCode\": \"NGN\",\n  \"subtotal\": \"80000.00\",\n  \"taxTotal\": \"6000.00\",\n  \"total\": \"86000.00\",\n  \"balanceDue\": \"86000.00\",\n  \"lines\": [\n    { \"itemName\": \"A4 paper\", \"qty\": \"20.000000\", \"rate\": \"4000.00\", \"lineNet\": \"80000.00\", \"lineTax\": \"6000.00\" }\n  ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/payments-made": {
      "get": {
        "operationId": "payments-made",
        "summary": "List payments made",
        "description": "Money out to suppliers.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/payments-made/{id}": {
      "get": {
        "operationId": "payment-made",
        "summary": "Get one payment made",
        "description": "A single payment and which bills it settled.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"PMT-000001\",\n  \"vendorName\": \"Office Supplies Ltd\",\n  \"date\": \"2026-08-10\",\n  \"amount\": \"86000.00\",\n  \"whtAmount\": \"0.00\",\n  \"method\": \"BANK_TRANSFER\",\n  \"bankAccountName\": \"Bank\",\n  \"currencyCode\": \"NGN\"\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/vendor-credits": {
      "get": {
        "operationId": "vendor-credits",
        "summary": "List vendor credits",
        "description": "Credits a supplier has given you.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/vendor-credits/{id}": {
      "get": {
        "operationId": "vendor-credit",
        "summary": "Get one vendor credit",
        "description": "A single vendor credit and its applications.",
        "tags": ["Purchasing &amp; payables"],
        "security": [ { "hubtollOAuth": ["purchasing:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"VC-000001\",\n  \"vendorName\": \"Office Supplies Ltd\",\n  \"date\": \"2026-08-12\",\n  \"status\": \"ISSUED\",\n  \"currencyCode\": \"NGN\",\n  \"total\": \"10000.00\",\n  \"appliedAmount\": \"10000.00\",\n  \"refundedAmount\": \"0.00\"\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/goods-receipts": {
      "get": {
        "operationId": "goods-receipts",
        "summary": "List goods receipts",
        "description": "Stock received against purchase orders.",
        "tags": ["Inventory"],
        "security": [ { "hubtollOAuth": ["inventory:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/goods-receipts/{id}": {
      "get": {
        "operationId": "goods-receipt",
        "summary": "Get one goods receipt",
        "description": "A single receipt including quantities and costs.",
        "tags": ["Inventory"],
        "security": [ { "hubtollOAuth": ["inventory:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"GRN-000002\",\n  \"vendorName\": \"Office Supplies Ltd\",\n  \"date\": \"2026-08-04\",\n  \"status\": \"POSTED\",\n  \"grniTotal\": \"120000.00\",\n  \"grniCleared\": \"120000.00\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/stock-adjustments": {
      "get": {
        "operationId": "stock-adjustments",
        "summary": "List stock adjustments",
        "description": "Corrections to on-hand quantities.",
        "tags": ["Inventory"],
        "security": [ { "hubtollOAuth": ["inventory:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/stock-adjustments/{id}": {
      "get": {
        "operationId": "stock-adjustment",
        "summary": "Get one stock adjustment",
        "description": "A single adjustment and its lines.",
        "tags": ["Inventory"],
        "security": [ { "hubtollOAuth": ["inventory:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"ADJ-000001\",\n  \"date\": \"2026-08-09\",\n  \"reason\": \"Stock count correction\",\n  \"status\": \"POSTED\",\n  \"lines\": [ … ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/accounts": {
      "get": {
        "operationId": "accounts",
        "summary": "Chart of accounts",
        "description": "Every ledger account with its code, type and role. Returns a plain array — a chart of accounts is small and complete. Balances are a question about a period, so they live on the reports, not here.",
        "tags": ["The books"],
        "security": [ { "hubtollOAuth": ["accounting:read"] } ],
        "parameters": [
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "[\n  {\n    \"id\": \"…\",\n    \"code\": \"1050\",\n    \"name\": \"Bank\",\n    \"rootType\": \"ASSET\",\n    \"currencyCode\": \"NGN\",\n    \"system\": true,\n    \"systemRole\": \"BANK_DEFAULT\",\n    \"active\": true\n  }\n]" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/journals": {
      "get": {
        "operationId": "journals",
        "summary": "List journal entries",
        "description": "Every posting to the ledger, newest first — including those raised automatically by invoices, bills and payments.",
        "tags": ["The books"],
        "security": [ { "hubtollOAuth": ["accounting:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/journals/{id}": {
      "get": {
        "operationId": "journal",
        "summary": "Get one journal entry",
        "description": "A single entry with all its debit and credit lines. Posted journals are immutable.",
        "tags": ["The books"],
        "security": [ { "hubtollOAuth": ["accounting:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"number\": \"JE-000009\",\n  \"postingDate\": \"2026-08-06\",\n  \"status\": \"POSTED\",\n  \"sourceType\": \"INVOICE_ISSUE\",\n  \"sourceNumber\": \"INV-000012\",\n  \"totalDebit\": \"450000.00\",\n  \"totalCredit\": \"450000.00\",\n  \"lines\": [\n    { \"accountCode\": \"1100\", \"accountName\": \"Accounts Receivable\", \"debit\": \"450000.00\", \"credit\": \"0.00\" },\n    { \"accountCode\": \"4000\", \"accountName\": \"Sales\", \"debit\": \"0.00\", \"credit\": \"450000.00\" }\n  ]\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/fiscal-years": {
      "get": {
        "operationId": "fiscal-years",
        "summary": "List fiscal years",
        "description": "Your financial years and their periods, with which are open, soft-closed or locked.",
        "tags": ["The books"],
        "security": [ { "hubtollOAuth": ["accounting:read"] } ],
        "parameters": [
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "[\n  { \"id\": \"…\", \"name\": \"2026\", \"startDate\": \"2026-01-01\", \"endDate\": \"2026-12-31\", \"closed\": false }\n]" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/reports/trial-balance": {
      "get": {
        "operationId": "trial-balance",
        "summary": "Trial balance",
        "description": "Debits and credits per account over a date range. Always balances — it is derived from the ledger, not stored.",
        "tags": ["The books"],
        "security": [ { "hubtollOAuth": ["accounting:read"] } ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, YYYY-MM-DD. Required.",
            "schema": { "type": "string", "format": "date" }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, YYYY-MM-DD, inclusive. Required.",
            "schema": { "type": "string", "format": "date" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"fromDate\": \"2026-01-01\",\n  \"toDate\": \"2026-12-31\",\n  \"rows\": [ … ],\n  \"totalDebit\": \"3100000.00\",\n  \"totalCredit\": \"3100000.00\"\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/reports/profit-and-loss": {
      "get": {
        "operationId": "profit-and-loss",
        "summary": "Profit &amp; loss",
        "description": "Income, cost of sales, gross profit, operating expenses and net profit for a period.",
        "tags": ["The books"],
        "security": [ { "hubtollOAuth": ["accounting:read"] } ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Start of the window, YYYY-MM-DD. Required.",
            "schema": { "type": "string", "format": "date" }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "End of the window, YYYY-MM-DD, inclusive. Required.",
            "schema": { "type": "string", "format": "date" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"fromDate\": \"2026-01-01\",\n  \"toDate\": \"2026-12-31\",\n  \"currency\": \"NGN\",\n  \"income\": [ … ],\n  \"netProfit\": \"820000.00\"\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/reports/balance-sheet": {
      "get": {
        "operationId": "balance-sheet",
        "summary": "Balance sheet",
        "description": "Assets, liabilities and equity as at a date, with a `balanced` flag that is true by construction: unclosed earnings are carried into equity.",
        "tags": ["The books"],
        "security": [ { "hubtollOAuth": ["accounting:read"] } ],
        "parameters": [
          {
            "name": "asOf",
            "in": "query",
            "required": false,
            "description": "The date to report as at, YYYY-MM-DD. Required.",
            "schema": { "type": "string", "format": "date" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"asOf\": \"2026-12-31\",\n  \"currency\": \"NGN\",\n  \"assets\": [ { \"code\": \"1050\", \"name\": \"Bank\", \"amount\": \"1250000.00\" } ],\n  \"liabilities\": [ … ],\n  \"equity\": [ … ],\n  \"totalAssets\": \"4200000.00\",\n  \"totalLiabilities\": \"1100000.00\",\n  \"totalEquity\": \"3100000.00\",\n  \"balanced\": true\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/staff": {
      "get": {
        "operationId": "staff",
        "summary": "List staff",
        "description": "Your employee directory. A credential sees the whole company — it has no personal records, so every row's \"mine\" flag comes back false.",
        "tags": ["People"],
        "security": [ { "hubtollOAuth": ["hr:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [\n    {\n      \"id\": \"…\",\n      \"firstName\": \"Amaka\",\n      \"lastName\": \"Obi\",\n      \"email\": \"amaka.obi@…\",\n      \"approvalStatus\": \"APPROVED\",\n      \"mine\": false\n    }\n  ],\n  \"totalElements\": 115\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/offices": {
      "get": {
        "operationId": "offices",
        "summary": "List office locations",
        "description": "Your office locations — useful for tagging records in another system with the same places.",
        "tags": ["People"],
        "security": [ { "hubtollOAuth": ["hr:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/holidays": {
      "get": {
        "operationId": "holidays",
        "summary": "List public holidays",
        "description": "The public holidays your company observes. A scheduling integration uses these so it does not book work on a company holiday.",
        "tags": ["People"],
        "security": [ { "hubtollOAuth": ["hr:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/leave-requests": {
      "get": {
        "operationId": "leave-requests",
        "summary": "List leave requests",
        "description": "Every leave request in the company, with its dates, category and approval status.",
        "tags": ["Leave"],
        "security": [ { "hubtollOAuth": ["leave:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [\n    {\n      \"id\": \"…\",\n      \"startDate\": \"2026-09-01\",\n      \"endDate\": \"2026-09-05\",\n      \"approvalStatus\": \"APPROVED\",\n      \"mine\": false\n    }\n  ],\n  \"totalElements\": 139\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/leave-categories": {
      "get": {
        "operationId": "leave-categories",
        "summary": "List leave categories",
        "description": "The active leave types your company has configured. Each carries its settings history (entitlement and rules live there). Returns a plain array.",
        "tags": ["Leave"],
        "security": [ { "hubtollOAuth": ["leave:read"] } ],
        "parameters": [
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "[\n  {\n    \"id\": \"…\",\n    \"name\": \"Annual Leave\",\n    \"description\": \"Paid annual leave\",\n    \"active\": true,\n    \"settings\": [ … ]\n  }\n]" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/companies": {
      "get": {
        "operationId": "crm-companies",
        "summary": "List CRM companies",
        "description": "The organisations in your pipeline.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/companies/{id}": {
      "get": {
        "operationId": "crm-company",
        "summary": "Get one CRM company",
        "description": "A single company with its owner, domain and custom-field answers.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"name\": \"Frames and heights\",\n  \"domain\": \"framesandheights.com\",\n  \"industry\": \"Construction\",\n  \"lifecycleStage\": \"CUSTOMER\",\n  \"ownerName\": \"Ian Stone\",\n  \"contactCount\": 3,\n  \"mine\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/contacts": {
      "get": {
        "operationId": "crm-contacts",
        "summary": "List CRM contacts",
        "description": "The people you deal with, and the company each belongs to.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/contacts/{id}": {
      "get": {
        "operationId": "crm-contact",
        "summary": "Get one CRM contact",
        "description": "A single contact.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"firstName\": \"Grace\",\n  \"lastName\": \"Wong\",\n  \"fullName\": \"Grace Wong\",\n  \"email\": \"grace@framesandheights.com\",\n  \"companyName\": \"Frames and heights\",\n  \"jobTitle\": \"Finance Lead\",\n  \"doNotEmail\": false,\n  \"mine\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/deals": {
      "get": {
        "operationId": "crm-deals",
        "summary": "List deals",
        "description": "Deals in flight with their stage, value and owner.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/deals/{id}": {
      "get": {
        "operationId": "crm-deal",
        "summary": "Get one deal",
        "description": "A single deal including its stage history.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"name\": \"Q3 fit-out\",\n  \"amount\": \"2500000.00\",\n  \"currencyCode\": \"NGN\",\n  \"stageName\": \"Negotiation\",\n  \"status\": \"OPEN\",\n  \"probability\": 60,\n  \"companyName\": \"Frames and heights\",\n  \"ownerName\": \"Ian Stone\",\n  \"expectedCloseDate\": \"2026-09-30\",\n  \"mine\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/leads": {
      "get": {
        "operationId": "crm-leads",
        "summary": "List leads",
        "description": "Inbound leads, their source and score.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 0. Defaults to 0.",
            "schema": { "type": "integer" }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Defaults to 10 on finance lists and 20 elsewhere — pass it explicitly. Maximum 500; larger values are clamped, not rejected.",
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"content\": [ … ],\n  \"size\": 20,\n  \"totalElements\": 42,\n  \"totalPages\": 3,\n  \"first\": true,\n  \"last\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/v1/crm/leads/{id}": {
      "get": {
        "operationId": "crm-lead",
        "summary": "Get one lead",
        "description": "A single lead.",
        "tags": ["CRM"],
        "security": [ { "hubtollOAuth": ["crm:read"] } ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The record id. Required — part of the path.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": { "application/json": { "schema": { "type": "object" }, "example": "{\n  \"id\": \"…\",\n  \"name\": \"Ada Eze\",\n  \"email\": \"ada@example.com\",\n  \"companyName\": \"Eze Logistics\",\n  \"source\": \"WEBSITE\",\n  \"score\": 42,\n  \"status\": \"NEW\",\n  \"mine\": false\n}" } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "hubtollOAuth": {
        "type": "oauth2",
        "description": "Client credentials. Send the credentials in the request BODY (client_secret_post); HTTP Basic is not supported on this endpoint.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://api.hubtoll.com/api/oauth/token",
            "scopes": {
              "sales:read": "Customers, items, quotes, sales orders, invoices, payments received, credit notes, sales receipts.",
              "purchasing:read": "Vendors, purchase requisitions, purchase orders, bills, payments made, vendor credits.",
              "inventory:read": "Goods receipts and stock adjustments.",
              "accounting:read": "Chart of accounts, journal entries, fiscal years, trial balance, profit & loss, balance sheet.",
              "hr:read": "Staff directory, office locations and public holidays.",
              "leave:read": "Leave requests and the leave categories your company has configured.",
              "crm:read": "Companies, contacts, deals and leads.",
              "sales:write": "Reserved. No write endpoint exists yet, so this grants nothing today.",
              "fin:read": "The API's original scope. Still works exactly as documented (customers, items, invoices); superseded by sales:read for anything new.",
              "fin:write": "Reserved from the API's first release. Grants nothing today; kept only so a token request naming it does not fail."
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every 4xx carries a human-readable message. Token-endpoint failures additionally carry an RFC 6749 error code.",
        "properties": {
          "message": { "type": "string", "description": "Safe to show a person. Missing/mistyped request parameters instead return a standard problem+json body whose detail field names the parameter." },
          "status": { "type": "string", "example": "401 UNAUTHORIZED" },
          "error": { "type": "string", "description": "Token endpoint only — invalid_client | invalid_scope | invalid_request | unsupported_grant_type." }
        }
      },
      "Page": {
        "type": "object",
        "description": "The envelope every list returns. It does not echo the requested page number — track it client-side.",
        "properties": {
          "content": { "type": "array", "items": {} },
          "size": { "type": "integer" },
          "totalElements": { "type": "integer" },
          "totalPages": { "type": "integer" },
          "first": { "type": "boolean" },
          "last": { "type": "boolean" }
        }
      }
    },
    "responses": {
      "BadRequest": { "description": "Malformed input — the message names what is wrong.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Missing, expired or revoked token. Get a new one and retry once.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "The token is valid but lacks the scope this endpoint needs. Retrying will not help.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "TooManyRequests": { "description": "Rate limited. Honour Retry-After.",
        "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait." } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  }
}
