{
    "openapi": "3.0.3",
    "info": {
        "title": "Pregota \u2014 School fees integration",
        "version": "1.0.0",
        "description": "Collect school fees on M-Pesa and keep a school's own software in step.\n\nPregota is not a school management system and is not trying to become one. Keep your roster, your marks, your timetables. We handle the money \u2014 collecting fees, tracking who has paid, paying the school out \u2014 and tell your system everything it needs to stay in step.\n\n**Build against the sandbox key first.** Same endpoints, same webhooks, no real school, no real phone, no real money.\n\n**Retries are free.** Send an `Idempotency-Key` on any POST and the same key returns the same answer; the work happens once.\n\n**The KES 1 gate.** A school cannot collect a shilling until its payout phone approves a KES 1 prompt on the handset. If a school says 'we're set up but nothing works', check `/verification` first."
    },
    "servers": [
        {
            "url": "https://pregota.com"
        }
    ],
    "tags": [
        {
            "name": "Vendor",
            "description": "One key, many schools."
        },
        {
            "name": "School",
            "description": "One school, its own software."
        },
        {
            "name": "Sandbox",
            "description": "Refused (403) on a live key."
        }
    ],
    "components": {
        "securitySchemes": {
            "ApiKey": {
                "type": "http",
                "scheme": "bearer",
                "description": "Vendor key (pgv_\u2026) or a school key (pgk_\u2026)."
            }
        },
        "parameters": {
            "SchoolRef": {
                "name": "school",
                "in": "path",
                "required": true,
                "schema": {
                    "type": "string"
                },
                "description": "The school_ref returned when the school was provisioned."
            },
            "IdempotencyKey": {
                "name": "Idempotency-Key",
                "in": "header",
                "required": false,
                "schema": {
                    "type": "string"
                },
                "description": "Same key \u21d2 same answer, work done once. Replays carry Idempotent-Replay: true."
            }
        },
        "schemas": {
            "Student": {
                "type": "object",
                "required": [
                    "admission_no",
                    "name",
                    "class"
                ],
                "properties": {
                    "admission_no": {
                        "type": "string",
                        "description": "THE identity of a pupil. Same number \u21d2 same child, always."
                    },
                    "name": {
                        "type": "string"
                    },
                    "class": {
                        "type": "string",
                        "description": "A property of the pupil, not part of who they are \u2014 change it and they MOVE (their paid history moves with them)."
                    },
                    "parent_phone": {
                        "type": "string",
                        "nullable": true,
                        "description": "Omit it and we keep what we hold \u2014 an absent guardian is silence, not a deletion."
                    },
                    "parent2_phone": {
                        "type": "string",
                        "nullable": true
                    }
                }
            },
            "RosterResult": {
                "type": "object",
                "properties": {
                    "received": {
                        "type": "integer"
                    },
                    "created": {
                        "type": "integer"
                    },
                    "updated": {
                        "type": "integer"
                    },
                    "unchanged": {
                        "type": "integer",
                        "description": "Recognised and left alone \u2014 a nightly resync of an unchanged roster writes nothing."
                    },
                    "skipped": {
                        "type": "array",
                        "items": {
                            "type": "object"
                        }
                    }
                }
            },
            "Verification": {
                "type": "object",
                "properties": {
                    "verified": {
                        "type": "boolean",
                        "description": "Until this is true the school cannot collect a shilling."
                    },
                    "prompt_sent": {
                        "type": "boolean",
                        "description": "false \u21d2 M-Pesa refused to send it. Fix the number and resend."
                    },
                    "payout_phone": {
                        "type": "string",
                        "nullable": true,
                        "description": "Masked (\u2022\u2022\u20225678)."
                    },
                    "hint": {
                        "type": "string",
                        "nullable": true
                    }
                }
            },
            "Reconciliation": {
                "type": "object",
                "properties": {
                    "checked": {
                        "type": "integer"
                    },
                    "agree": {
                        "type": "integer"
                    },
                    "in_agreement": {
                        "type": "boolean"
                    },
                    "differences": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "admission_no": {
                                    "type": "string"
                                },
                                "kind": {
                                    "type": "string",
                                    "enum": [
                                        "balance_differs",
                                        "unknown_to_pregota",
                                        "unknown_fee"
                                    ]
                                },
                                "theirs": {
                                    "type": "integer",
                                    "nullable": true
                                },
                                "ours": {
                                    "type": "integer",
                                    "nullable": true
                                },
                                "delta": {
                                    "type": "integer"
                                },
                                "hint": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "missing_from_your_list": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Pupils WE hold that your list never mentioned \u2014 as dangerous as the reverse."
                    }
                }
            },
            "Overview": {
                "type": "object",
                "description": "The whole school in one call \u2014 the payload behind a dashboard. Note money_now: what a school HAS is not what it has collected. Money already paid out is gone, and cash never reached a wallet at all \u2014 neither is derivable from the payments feed.",
                "properties": {
                    "school": {
                        "type": "object",
                        "properties": {
                            "name": {
                                "type": "string"
                            },
                            "fees": {
                                "type": "integer",
                                "description": "How many fee types the school runs."
                            }
                        }
                    },
                    "pupils": {
                        "type": "object",
                        "properties": {
                            "total": {
                                "type": "integer"
                            },
                            "fully_paid": {
                                "type": "integer"
                            },
                            "outstanding": {
                                "type": "integer",
                                "description": "Pupils carrying a balance."
                            }
                        }
                    },
                    "collected": {
                        "type": "object",
                        "description": "Lifetime, in whole KES. A running total, NOT a balance.",
                        "properties": {
                            "expected": {
                                "type": "integer"
                            },
                            "total": {
                                "type": "integer"
                            },
                            "remaining": {
                                "type": "integer"
                            },
                            "mpesa": {
                                "type": "integer"
                            },
                            "cash": {
                                "type": "integer"
                            }
                        }
                    },
                    "money_now": {
                        "type": "object",
                        "description": "Where the money actually is today.",
                        "properties": {
                            "in_wallets": {
                                "type": "integer",
                                "description": "Sitting in the ring-fenced fee wallets right now."
                            },
                            "spendable": {
                                "type": "integer",
                                "description": "in_wallets less meals_float \u2014 what the school may actually pay out."
                            },
                            "paid_out": {
                                "type": "integer",
                                "description": "Already withdrawn to the school's M-Pesa."
                            },
                            "cash_with_teachers": {
                                "type": "integer",
                                "description": "Collected as cash and not yet handed to the office. Never in a wallet, never payable out."
                            },
                            "cash_unattributed": {
                                "type": "integer",
                                "description": "Cash held but recorded against no teacher (taken at the office)."
                            },
                            "meals_float": {
                                "type": "integer",
                                "description": "Parents' prepaid meal money not yet eaten. Held by the school but NOT the school's to spend."
                            },
                            "wallets": {
                                "type": "array",
                                "description": "One ring-fenced wallet per fee. A school's money is not one pot.",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "fee_id": {
                                            "type": "integer"
                                        },
                                        "name": {
                                            "type": "string"
                                        },
                                        "balance": {
                                            "type": "integer"
                                        },
                                        "paid_out": {
                                            "type": "integer"
                                        },
                                        "held_by": {
                                            "type": "string",
                                            "description": "school office, delegated teacher, delegated class teachers, or not yet collected."
                                        }
                                    }
                                }
                            },
                            "cash_holders": {
                                "type": "array",
                                "description": "Who is holding cash, by name \u2014 a single anonymous total cannot be chased.",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "teacher_id": {
                                            "type": "integer"
                                        },
                                        "name": {
                                            "type": "string"
                                        },
                                        "cash_owed": {
                                            "type": "integer"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "classes": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "class": {
                                    "type": "string"
                                },
                                "pupils": {
                                    "type": "integer"
                                },
                                "outstanding": {
                                    "type": "integer"
                                },
                                "collected": {
                                    "type": "integer"
                                },
                                "remaining": {
                                    "type": "integer"
                                },
                                "expected": {
                                    "type": "integer"
                                }
                            }
                        }
                    }
                }
            },
            "Statement": {
                "type": "object",
                "description": "Billed, collected, paid out \u2014 and what the wallet's own ledger recorded, so the two can be held against each other.",
                "properties": {
                    "from": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "to": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "fees": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "fee_id": {
                                    "type": "integer"
                                },
                                "name": {
                                    "type": "string"
                                },
                                "billed": {
                                    "type": "object"
                                },
                                "collected": {
                                    "type": "object",
                                    "description": "M-Pesa held apart from cash: cash never enters the wallet and can never be paid out."
                                },
                                "outstanding": {
                                    "type": "integer",
                                    "nullable": true
                                },
                                "wallet": {
                                    "type": "object",
                                    "description": "opening / in / out / closing, from the ring-fenced wallet's own ledger."
                                },
                                "receipts": {
                                    "type": "array",
                                    "items": {
                                        "type": "object"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "security": [
        {
            "ApiKey": []
        }
    ],
    "paths": {
        "/api/sms/v1/schools": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Every school you run",
                "operationId": "VendorListSchools",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                }
            },
            "post": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Provision a school",
                "operationId": "VendorProvisionSchool",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Starts the KES 1 prompt to the payout phone. Check prompt_sent \u2014 false means M-Pesa refused and the school cannot collect until it is resent.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/IdempotencyKey"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "school_name",
                                    "admin_name",
                                    "payout_phone",
                                    "fees",
                                    "classes"
                                ],
                                "properties": {
                                    "school_name": {
                                        "type": "string"
                                    },
                                    "admin_name": {
                                        "type": "string"
                                    },
                                    "payout_phone": {
                                        "type": "string"
                                    },
                                    "fees": {
                                        "type": "string"
                                    },
                                    "classes": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/sms/v1/schools/{school}/roster": {
            "post": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Push the pupil list",
                "operationId": "VendorUpsertRoster",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/RosterResult"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Send the WHOLE roster whenever it changes. Matched on admission number, so repeats are safe and a promoted pupil moves rather than being cloned.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    },
                    {
                        "$ref": "#/components/parameters/IdempotencyKey"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/overview": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "The whole school (dashboard)",
                "operationId": "VendorOverview",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Overview"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "One call to draw the school its own dashboard. Wallet balances, money already paid out and cash held by teachers CANNOT be derived from the payments feed \u2014 read them here.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/wallet": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Wallet position (the balance now)",
                "operationId": "VendorWallet",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Just the money_now block from overview: what the school can pay out RIGHT NOW. in_wallets is the only spendable figure; cash never enters a wallet, and meals_float is parents' prepaid meal money, held but not the school's to spend.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/fees": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Fee structure",
                "operationId": "VendorFees",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/payments": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Payment feed (catch-up)",
                "operationId": "VendorPayments",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/students/{admissionNo}": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "One pupil's standing",
                "operationId": "VendorStudent",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    },
                    {
                        "name": "admissionNo",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/statement": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "The bursar's statement",
                "operationId": "VendorStatement",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Statement"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    },
                    {
                        "name": "date",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        },
                        "description": "Defaults to today. Or pass from/to."
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/reconcile": {
            "post": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Do we agree?",
                "operationId": "VendorReconcile",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Reconciliation"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Send what YOUR system thinks each pupil owes; we say precisely where we differ.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "students"
                                ],
                                "properties": {
                                    "students": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/sms/v1/schools/{school}/payments/stk": {
            "post": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Ring a parent's phone for a fee",
                "operationId": "VendorRequestPayment",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Prompts the parent on their handset from inside YOUR software. Leave phone out \u2014 we use the guardian on the roster. If you do send one it must MATCH a guardian on that pupil's row (parent_phone or parent2_phone), else 403 not_the_guardian: a fee prompt is an M-Pesa PIN request with a school's name on it, so it only ever goes to that pupil's own parent. fee_id is required only when the pupil's class pays more than one open fee (we refuse to guess). 202 = the phone is ringing; NOTHING is collected until they approve, and your payment.recorded webhook is what tells you they did. ALWAYS send an Idempotency-Key: a retry must not ring the parent twice. 60/min. Spelling-tolerant: stkpush, stk-push, stk_push or plain stk, with or without the /payments/ prefix, all reach this endpoint.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    },
                    {
                        "$ref": "#/components/parameters/IdempotencyKey"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "admission_no",
                                    "amount",
                                    "phone",
                                    "fee_id"
                                ],
                                "properties": {
                                    "admission_no": {
                                        "type": "string"
                                    },
                                    "amount": {
                                        "type": "string"
                                    },
                                    "phone": {
                                        "type": "string"
                                    },
                                    "fee_id": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/sms/v1/schools/{school}/payments/{payment}/status": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Has the parent approved yet?",
                "operationId": "VendorPaymentStatus",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "For a parent PORTAL: the webhook tells your server, but this is what turns the spinner in front of the parent into a receipt. state = pending | paid | failed. It also recovers a LOST M-Pesa callback by asking Safaricom directly, so a parent who has paid is never left staring at \"check your phone\". Poll every 3-5s for about a minute.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    },
                    {
                        "name": "payment",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "payment_id from the STK response"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/parent-access": {
            "post": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Let a parent in \u2014 no prompt, no PIN, no KES 1",
                "operationId": "VendorParentAccess",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "SEND PARENTS HERE. Do NOT send them to Pregota's own parent login: it pushes KES 1 at their handset and asks for an M-Pesa PIN just to show a fee balance, which reads as a scam and puts them off. You have already signed them in and your roster already carries their number \u2014 so we take your word for it. Returns a signed `url`: redirect the parent to it and they land on their own fee record. It names ONE parent at ONE school and expires (default 15 minutes, max 60) \u2014 it is a key to a family's fee record, so treat it like one and mint it fresh each time.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "admission_no",
                                    "minutes"
                                ],
                                "properties": {
                                    "admission_no": {
                                        "type": "string"
                                    },
                                    "minutes": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/sms/v1/schools/{school}/prompts": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Why didn't the parent's phone ring?",
                "operationId": "VendorPrompts",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "READ THIS FIRST when a parent reports no prompt. Every ask-to-pay and what became of it \u2014 the successes too, so \"we sent it and she cancelled\" is distinguishable from \"we never sent it\". And note the empty case: if a parent tapped Pay in YOUR portal and nothing appears here, the request never reached us.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/verification": {
            "get": {
                "tags": [
                    "Vendor"
                ],
                "summary": "The KES 1 gate",
                "operationId": "VendorVerification",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Verification"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/verification/resend": {
            "post": {
                "tags": [
                    "Vendor"
                ],
                "summary": "Send the KES 1 prompt again",
                "operationId": "VendorResendVerification",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "The answer to \"the school integrated and never got the STK\". Rate limited to 5/min.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/sandbox/approve": {
            "post": {
                "tags": [
                    "Sandbox"
                ],
                "summary": "Approve the KES 1 prompt",
                "operationId": "SandboxSandboxApprove",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Stands in for the head teacher's handset. 403 on a live key.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ]
            }
        },
        "/api/sms/v1/schools/{school}/sandbox/payments": {
            "post": {
                "tags": [
                    "Sandbox"
                ],
                "summary": "Make a parent pay",
                "operationId": "SandboxSandboxPayment",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Your webhook fires and the balance moves, exactly as in production. 403 on a live key.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/SchoolRef"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "admission_no",
                                    "amount"
                                ],
                                "properties": {
                                    "admission_no": {
                                        "type": "string"
                                    },
                                    "amount": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/school/v1/roster": {
            "post": {
                "tags": [
                    "School"
                ],
                "summary": "Push the pupil list",
                "operationId": "SchoolUpsertRoster",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/RosterResult"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "parameters": [
                    {
                        "$ref": "#/components/parameters/IdempotencyKey"
                    }
                ]
            }
        },
        "/api/school/v1/overview": {
            "get": {
                "tags": [
                    "School"
                ],
                "summary": "The whole school (dashboard)",
                "operationId": "SchoolOverview",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Overview"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "Wallet balances, money already paid out and cash held by teachers \u2014 none of it derivable from the payments feed."
            }
        },
        "/api/school/v1/payments/stk": {
            "post": {
                "tags": [
                    "School"
                ],
                "summary": "Ring a parent's phone for a fee",
                "operationId": "SchoolRequestPayment",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "The bursar is already looking at the child; this puts the prompt on the parent's handset without leaving your screen. Same money path, same KES-1 payout gate as everything else. The prompt goes to the guardian on the pupil's roster row \u2014 omit phone, or send one that matches it (anything else is 403 not_the_guardian). 202 = the phone is ringing; nothing is collected until they approve. 30/min.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/IdempotencyKey"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "admission_no",
                                    "amount",
                                    "phone",
                                    "fee_id"
                                ],
                                "properties": {
                                    "admission_no": {
                                        "type": "string"
                                    },
                                    "amount": {
                                        "type": "string"
                                    },
                                    "phone": {
                                        "type": "string"
                                    },
                                    "fee_id": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/school/v1/payments/{payment}/status": {
            "get": {
                "tags": [
                    "School"
                ],
                "summary": "Has the parent approved yet?",
                "operationId": "SchoolPaymentStatus",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "description": "The poll for a parent portal. Also recovers a lost M-Pesa callback. state = pending | paid | failed.",
                "parameters": [
                    {
                        "name": "payment",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "payment_id from the STK response"
                    }
                ]
            }
        },
        "/api/school/v1/fees": {
            "get": {
                "tags": [
                    "School"
                ],
                "summary": "Fee structure",
                "operationId": "SchoolFees",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                }
            }
        },
        "/api/school/v1/payments": {
            "get": {
                "tags": [
                    "School"
                ],
                "summary": "Payment feed (catch-up)",
                "operationId": "SchoolPayments",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                }
            }
        },
        "/api/school/v1/students/{admissionNo}": {
            "get": {
                "tags": [
                    "School"
                ],
                "summary": "One pupil's standing",
                "operationId": "SchoolStudent",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "parameters": [
                    {
                        "name": "admissionNo",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ]
            }
        },
        "/api/school/v1/statement": {
            "get": {
                "tags": [
                    "School"
                ],
                "summary": "The bursar's statement",
                "operationId": "SchoolStatement",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Statement"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                }
            }
        },
        "/api/school/v1/reconcile": {
            "post": {
                "tags": [
                    "School"
                ],
                "summary": "Do we agree?",
                "operationId": "SchoolReconcile",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Reconciliation"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                },
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "students"
                                ],
                                "properties": {
                                    "students": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/school/v1/verification": {
            "get": {
                "tags": [
                    "School"
                ],
                "summary": "The KES 1 gate",
                "operationId": "SchoolVerification",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Verification"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                }
            }
        },
        "/api/school/v1/verification/resend": {
            "post": {
                "tags": [
                    "School"
                ],
                "summary": "Send the KES 1 prompt again",
                "operationId": "SchoolResendVerification",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "invalid_api_key"
                    },
                    "404": {
                        "description": "Not yours, or not found."
                    }
                }
            }
        }
    }
}