{
    "openapi": "3.1.0",
    "info": {
        "title": "GCX SHS Allocation API",
        "version": "1.0.0",
        "description": "Read-only access to GCX supplier summaries for connected systems.\n\n**Authentication**: send the key issued on the app's API access page as `Authorization: Bearer <key>` (or `X-API-Key: <key>`). Never put a key in the URL. Keys are for server-to-server use only.\n\n**Amounts** are decimal strings in GHS with 2 places; quantities have 3 places. **Totals** in `meta.totals` always cover all active allocations, regardless of filters.\n\n**Polling**: store the `ETag` and send it as `If-None-Match`; you get `304 Not Modified` until allocation data changes.\n\n**Errors** always return `{\"error\": {\"code\", \"message\", \"status\", \"request_id\"}}`. Branch on `code`, not on the message."
    },
    "servers": [
        {
            "url": "https://gcxapps.net/allocationtool/api/v1"
        }
    ],
    "security": [
        {
            "bearerAuth": []
        },
        {
            "apiKeyHeader": []
        }
    ],
    "tags": [
        {
            "name": "Supplier summaries"
        },
        {
            "name": "Service"
        }
    ],
    "paths": {
        "/supplier-summaries": {
            "get": {
                "tags": [
                    "Supplier summaries"
                ],
                "operationId": "listSupplierSummaries",
                "summary": "List supplier summaries",
                "description": "Suppliers with active allocations, with contract value, 1% fee, school and line counts, and item breakdown. JSON is paged; CSV returns every matching supplier.",
                "x-required-scope": "summaries:read",
                "parameters": [
                    {
                        "name": "q",
                        "in": "query",
                        "required": false,
                        "description": "Case-insensitive text contained in the supplier name.",
                        "schema": {
                            "type": "string",
                            "maxLength": 200
                        }
                    },
                    {
                        "name": "supplier",
                        "in": "query",
                        "required": false,
                        "description": "Exact supplier name (case-insensitive). Use this for names containing \"/\".",
                        "schema": {
                            "type": "string",
                            "maxLength": 200
                        }
                    },
                    {
                        "name": "sort",
                        "in": "query",
                        "required": false,
                        "description": "Sort field.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "name",
                                "value",
                                "lines",
                                "schools"
                            ],
                            "default": "name"
                        }
                    },
                    {
                        "name": "order",
                        "in": "query",
                        "required": false,
                        "description": "Sort direction. Defaults to asc for name, desc otherwise.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "asc",
                                "desc"
                            ]
                        }
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "description": "Page number (JSON only).",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "default": 1
                        }
                    },
                    {
                        "name": "per_page",
                        "in": "query",
                        "required": false,
                        "description": "Suppliers per page (JSON only).",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 500,
                            "default": 100
                        }
                    },
                    {
                        "name": "include",
                        "in": "query",
                        "required": false,
                        "description": "Include the per-item breakdown.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "items",
                                "none"
                            ],
                            "default": "items"
                        }
                    },
                    {
                        "name": "format",
                        "in": "query",
                        "required": false,
                        "description": "Response format. Also selectable with the .csv suffix (list only) or Accept: text/csv.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "json",
                                "csv"
                            ],
                            "default": "json"
                        }
                    },
                    {
                        "name": "layout",
                        "in": "query",
                        "required": false,
                        "description": "CSV layout. \"report\" is identical to the app export (with TOTAL rows and blank separators); \"flat\" has one row per supplier and item for machine import.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "report",
                                "flat"
                            ],
                            "default": "report"
                        }
                    },
                    {
                        "name": "If-None-Match",
                        "in": "header",
                        "required": false,
                        "description": "ETag from a previous response.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Supplier summaries.",
                        "headers": {
                            "X-RateLimit-Limit": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Requests allowed per minute for this key."
                            },
                            "X-RateLimit-Remaining": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Requests left in the current minute."
                            },
                            "X-RateLimit-Reset": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Unix time when the window resets."
                            },
                            "ETag": {
                                "schema": {
                                    "type": "string"
                                },
                                "description": "Send back as If-None-Match to receive 304 when nothing changed."
                            },
                            "X-Data-Version": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Allocation data version this response was built from."
                            },
                            "X-Request-Id": {
                                "schema": {
                                    "type": "string"
                                },
                                "description": "Quote this when reporting a problem."
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/SupplierSummaryList"
                                }
                            },
                            "text/csv": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "﻿Supplier,Item,Total Value (GHS),Total Qty,Unit,Net Price/Unit,Schools,1% Fee (GHS)\r\nACME FOODS,RICE,1000.00,10,Bag,30.0000,10,10.00\r\nACME FOODS,TOTAL,1000.00,,,,10,10.00\r\n"
                            }
                        }
                    },
                    "304": {
                        "description": "Not modified since the ETag you sent."
                    },
                    "400": {
                        "description": "Invalid query parameter (code: invalid_parameter, key_in_url).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, invalid, expired, or revoked key (code: missing_api_key, invalid_api_key, api_key_expired, api_key_revoked).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Key lacks the scope or the caller IP is not allowed (code: insufficient_scope, ip_not_allowed).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limit exceeded (code: rate_limited, too_many_failed_attempts). Honour Retry-After.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "Temporarily unavailable (code: database_unavailable, not_configured).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/supplier-summaries.csv": {
            "get": {
                "tags": [
                    "Supplier summaries"
                ],
                "operationId": "downloadSupplierSummaryCsv",
                "summary": "Supplier Summary CSV",
                "description": "The same CSV the app exports from Supplier Summary. Accepts the same filters as the list, except paging.",
                "x-required-scope": "summaries:read",
                "parameters": [
                    {
                        "name": "q",
                        "in": "query",
                        "required": false,
                        "description": "Text contained in the supplier name.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "supplier",
                        "in": "query",
                        "required": false,
                        "description": "Exact supplier name.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "sort",
                        "in": "query",
                        "required": false,
                        "description": "Sort field.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "name",
                                "value",
                                "lines",
                                "schools"
                            ]
                        }
                    },
                    {
                        "name": "order",
                        "in": "query",
                        "required": false,
                        "description": "Sort direction.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "asc",
                                "desc"
                            ]
                        }
                    },
                    {
                        "name": "layout",
                        "in": "query",
                        "required": false,
                        "description": "CSV layout. \"report\" is identical to the app export (with TOTAL rows and blank separators); \"flat\" has one row per supplier and item for machine import.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "report",
                                "flat"
                            ],
                            "default": "report"
                        }
                    },
                    {
                        "name": "If-None-Match",
                        "in": "header",
                        "required": false,
                        "description": "ETag from a previous response.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "CSV file (UTF-8 with BOM, CRLF line endings).",
                        "headers": {
                            "X-RateLimit-Limit": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Requests allowed per minute for this key."
                            },
                            "X-RateLimit-Remaining": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Requests left in the current minute."
                            },
                            "X-RateLimit-Reset": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Unix time when the window resets."
                            },
                            "ETag": {
                                "schema": {
                                    "type": "string"
                                },
                                "description": "Send back as If-None-Match to receive 304 when nothing changed."
                            },
                            "X-Data-Version": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Allocation data version this response was built from."
                            },
                            "X-Request-Id": {
                                "schema": {
                                    "type": "string"
                                },
                                "description": "Quote this when reporting a problem."
                            }
                        },
                        "content": {
                            "text/csv": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "﻿Supplier,Item,Total Value (GHS),Total Qty,Unit,Net Price/Unit,Schools,1% Fee (GHS)\r\nACME FOODS,RICE,1000.00,10,Bag,30.0000,10,10.00\r\nACME FOODS,TOTAL,1000.00,,,,10,10.00\r\n"
                            }
                        }
                    },
                    "304": {
                        "description": "Not modified."
                    },
                    "400": {
                        "description": "Invalid query parameter (code: invalid_parameter, key_in_url).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, invalid, expired, or revoked key (code: missing_api_key, invalid_api_key, api_key_expired, api_key_revoked).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Key lacks the scope or the caller IP is not allowed (code: insufficient_scope, ip_not_allowed).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limit exceeded (code: rate_limited, too_many_failed_attempts). Honour Retry-After.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "Temporarily unavailable (code: database_unavailable, not_configured).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/supplier-summaries/{supplier}": {
            "get": {
                "tags": [
                    "Supplier summaries"
                ],
                "operationId": "getSupplierSummary",
                "summary": "One supplier summary",
                "x-required-scope": "summaries:read",
                "parameters": [
                    {
                        "name": "supplier",
                        "in": "path",
                        "required": true,
                        "description": "Exact supplier name, URL-encoded. An exact match wins; otherwise a unique case-insensitive match is used.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "include",
                        "in": "query",
                        "required": false,
                        "description": "Include the per-item breakdown.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "items",
                                "none"
                            ],
                            "default": "items"
                        }
                    },
                    {
                        "name": "format",
                        "in": "query",
                        "required": false,
                        "description": "Response format. Also selectable with the .csv suffix (list only) or Accept: text/csv.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "json",
                                "csv"
                            ],
                            "default": "json"
                        }
                    },
                    {
                        "name": "layout",
                        "in": "query",
                        "required": false,
                        "description": "CSV layout. \"report\" is identical to the app export (with TOTAL rows and blank separators); \"flat\" has one row per supplier and item for machine import.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "report",
                                "flat"
                            ],
                            "default": "report"
                        }
                    },
                    {
                        "name": "If-None-Match",
                        "in": "header",
                        "required": false,
                        "description": "ETag from a previous response.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The supplier.",
                        "headers": {
                            "X-RateLimit-Limit": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Requests allowed per minute for this key."
                            },
                            "X-RateLimit-Remaining": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Requests left in the current minute."
                            },
                            "X-RateLimit-Reset": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Unix time when the window resets."
                            },
                            "ETag": {
                                "schema": {
                                    "type": "string"
                                },
                                "description": "Send back as If-None-Match to receive 304 when nothing changed."
                            },
                            "X-Data-Version": {
                                "schema": {
                                    "type": "integer"
                                },
                                "description": "Allocation data version this response was built from."
                            },
                            "X-Request-Id": {
                                "schema": {
                                    "type": "string"
                                },
                                "description": "Quote this when reporting a problem."
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data",
                                        "meta"
                                    ],
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/SupplierSummary"
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/SummaryMeta"
                                        }
                                    }
                                }
                            },
                            "text/csv": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "﻿Supplier,Item,Total Value (GHS),Total Qty,Unit,Net Price/Unit,Schools,1% Fee (GHS)\r\nACME FOODS,RICE,1000.00,10,Bag,30.0000,10,10.00\r\nACME FOODS,TOTAL,1000.00,,,,10,10.00\r\n"
                            }
                        }
                    },
                    "304": {
                        "description": "Not modified."
                    },
                    "404": {
                        "description": "No supplier with active allocations has this name (code: supplier_not_found).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The name matches several suppliers with different capitalization (code: ambiguous_supplier).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Invalid query parameter (code: invalid_parameter, key_in_url).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, invalid, expired, or revoked key (code: missing_api_key, invalid_api_key, api_key_expired, api_key_revoked).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Key lacks the scope or the caller IP is not allowed (code: insufficient_scope, ip_not_allowed).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limit exceeded (code: rate_limited, too_many_failed_attempts). Honour Retry-After.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "Temporarily unavailable (code: database_unavailable, not_configured).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/health": {
            "get": {
                "tags": [
                    "Service"
                ],
                "operationId": "health",
                "summary": "Service status",
                "security": [],
                "responses": {
                    "200": {
                        "description": "The API and database are reachable.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "status": {
                                            "type": "string",
                                            "const": "ok"
                                        },
                                        "api_version": {
                                            "type": "string"
                                        },
                                        "data_version": {
                                            "type": "integer"
                                        },
                                        "time": {
                                            "type": "string",
                                            "format": "date-time"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "Temporarily unavailable (code: database_unavailable, not_configured).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/openapi.json": {
            "get": {
                "tags": [
                    "Service"
                ],
                "operationId": "openapi",
                "summary": "This document",
                "security": [],
                "responses": {
                    "200": {
                        "description": "OpenAPI 3.1 document."
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "gcx_live_<prefix>_<secret>"
            },
            "apiKeyHeader": {
                "type": "apiKey",
                "in": "header",
                "name": "X-API-Key"
            }
        },
        "schemas": {
            "Money": {
                "type": "string",
                "pattern": "^-?\\d+\\.\\d{2}$",
                "description": "Decimal amount in GHS as a string, exactly 2 decimal places.",
                "examples": [
                    "1900.00"
                ]
            },
            "SupplierItem": {
                "type": "object",
                "required": [
                    "item",
                    "value",
                    "fee",
                    "quantity",
                    "unit",
                    "net_price_per_unit",
                    "school_count",
                    "active_lines"
                ],
                "properties": {
                    "item": {
                        "type": "string",
                        "examples": [
                            "RICE"
                        ]
                    },
                    "value": {
                        "$ref": "#/components/schemas/Money",
                        "description": "Sum of school-line amounts. Each line is ceiled quantity × net price, rounded half up to two decimals before summing; combined quantity × net may differ by a few pesewas."
                    },
                    "fee": {
                        "$ref": "#/components/schemas/Money"
                    },
                    "quantity": {
                        "type": "string",
                        "pattern": "^\\d+\\.\\d{3}$",
                        "examples": [
                            "10.000"
                        ]
                    },
                    "unit": {
                        "type": "string",
                        "examples": [
                            "Bag"
                        ]
                    },
                    "net_price_per_unit": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Current net price from the price table, 4 decimal places.",
                        "examples": [
                            "30.0000"
                        ]
                    },
                    "school_count": {
                        "type": "integer"
                    },
                    "active_lines": {
                        "type": "integer"
                    }
                }
            },
            "SupplierSummary": {
                "type": "object",
                "required": [
                    "supplier",
                    "contract_value",
                    "fee",
                    "school_count",
                    "active_lines",
                    "item_count"
                ],
                "properties": {
                    "supplier": {
                        "type": "string",
                        "examples": [
                            "ACME FOODS"
                        ]
                    },
                    "contract_value": {
                        "$ref": "#/components/schemas/Money"
                    },
                    "fee": {
                        "$ref": "#/components/schemas/Money",
                        "description": "1% of contract_value, rounded half up."
                    },
                    "school_count": {
                        "type": "integer"
                    },
                    "active_lines": {
                        "type": "integer"
                    },
                    "item_count": {
                        "type": "integer"
                    },
                    "items": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/SupplierItem"
                        },
                        "description": "Omitted when include=none."
                    }
                }
            },
            "SummaryMeta": {
                "type": "object",
                "properties": {
                    "currency": {
                        "type": "string",
                        "const": "GHS"
                    },
                    "fee_rate": {
                        "type": "string",
                        "examples": [
                            "0.01"
                        ]
                    },
                    "line_value_rounding": {
                        "type": "string",
                        "const": "half_up_2dp_per_school_line"
                    },
                    "data_version": {
                        "type": "integer"
                    },
                    "generated_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "totals": {
                        "type": "object",
                        "properties": {
                            "programme_value": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "allocated_value": {
                                "$ref": "#/components/schemas/Money"
                            },
                            "available_value": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "supplier_count": {
                                "type": "integer"
                            },
                            "active_lines": {
                                "type": "integer"
                            }
                        }
                    },
                    "pagination": {
                        "type": "object",
                        "properties": {
                            "page": {
                                "type": "integer"
                            },
                            "per_page": {
                                "type": "integer"
                            },
                            "total": {
                                "type": "integer"
                            },
                            "total_pages": {
                                "type": "integer"
                            }
                        }
                    }
                }
            },
            "SupplierSummaryList": {
                "type": "object",
                "required": [
                    "data",
                    "meta",
                    "links"
                ],
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/SupplierSummary"
                        }
                    },
                    "meta": {
                        "$ref": "#/components/schemas/SummaryMeta"
                    },
                    "links": {
                        "type": "object",
                        "properties": {
                            "self": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "uri"
                            },
                            "first": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "uri"
                            },
                            "last": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "uri"
                            },
                            "prev": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "uri"
                            },
                            "next": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "format": "uri"
                            }
                        }
                    }
                }
            },
            "Error": {
                "type": "object",
                "required": [
                    "error"
                ],
                "properties": {
                    "error": {
                        "type": "object",
                        "required": [
                            "code",
                            "message",
                            "status"
                        ],
                        "properties": {
                            "code": {
                                "type": "string",
                                "examples": [
                                    "invalid_api_key"
                                ]
                            },
                            "message": {
                                "type": "string"
                            },
                            "status": {
                                "type": "integer"
                            },
                            "request_id": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    }
                }
            }
        }
    }
}