{
    "openapi": "3.0.0",
    "info": {
        "title": "Kapitano — Store Portal API",
        "description": "The **store portal** (`/api/store-portal/*`): where a shop's own staff sign in, configure\ntheir integration, and read their orders and their money.\n\nDistinct from the [store integration API](/api/documentation/integration), which is what\ntheir *servers* call. Different guard, different credentials, different audience: one is a\nmachine holding a signed token, this is a person holding a password.\n\n---\n\n## 1. Required headers\n\nEvery request needs both of these, enforced globally **before** authentication — which is why\na missing header answers `401` rather than `406`.\n\n| Header | Value |\n|---|---|\n| `Accept` | `application/json` |\n| `Accept-Language` | `en` or `ar` — the portal is read by people, so send the user's language |\n| `Authorization` | `Bearer <token>`, on everything except the `auth/*` endpoints |\n\nUnlike the machine API, `Accept-Language` here should be **the signed-in person's** language.\n\n---\n\n## 2. Onboarding, and what is reachable when\n\nA shop registers itself. What that creates is an **application**, never a live integration:\nno API token exists, no order can arrive and no callback can leave until an administrator\napproves it.\n\n```\nPOST /auth/register  →  store: pending, inactive        owner: pending\n         │\n         ├─ the owner may sign in immediately, and reach /settings/* only\n         │\n   an administrator reviews\n         │\n         ├─ approved   →  everything below opens; the API token is issued separately\n         ├─ rejected   →  sign-in refused\n         └─ suspended  →  sign-in refused, tokens revoked, on the next request\n```\n\n**A pending owner can sign in on purpose.** They can do nothing harmful and they can do the\none useful thing: set their callback URL and send themselves a test event, so their receiver\nis proved before the approval rather than after it.\n\n| Screen | Needs an approved store? |\n|---|---|\n| `profile/*`, `settings/*`, `staff/*` | no |\n| `orders/*`, `webhooks/*`, `payments/*` | **yes** — `403 store_not_approved` until then |\n\n---\n\n## 3. Roles\n\n| Role | May |\n|---|---|\n| `owner` | everything, plus settings, secrets, the IP allowlist, replaying a webhook, and inviting colleagues |\n| `staff` | read orders, the timeline, the delivery log and the statement |\n\nWhat separates them is real: changing `webhook_url` redirects the shop's order data to a new\naddress, and rotating a secret can stop their live integration. Reading a report cannot.\n`403 owner_only` when a staff account reaches an owner's route.\n\nEvery response from `profile` and `login` carries `can_manage_integration`, so a screen can\ndecide what to draw without duplicating this table.\n\n---\n\n## 4. Secrets\n\n`GET /settings` returns **metadata only** — whether secrets exist, when they were last\nrotated, when the previous pair stops working. The values themselves come from\n`POST /settings/secrets/reveal`, which asks for the caller's password again: a stolen session\nmust not be worth as much as a stolen signing key. That response carries `Cache-Control:\nno-store`.\n\n**Rotation does not cause an outage.** For 24 hours afterwards we accept either secret on\ninbound requests, and every outbound callback is signed with **both** — the signature header\nthen carries two `v1` values. A verifier that reads only the first will start rejecting\ncallbacks the moment somebody rotates; see the integration spec, §8.4.\n\n---\n\n## 5. Tenancy\n\nNothing in this API takes a store identifier. The store is resolved from the caller's own\ntoken, so there is no parameter anywhere that could be pointed at somebody else's data.\n\nAnything belonging to another store answers **`404`, never `403`** — a 403 would confirm that\nthe record exists.\n\n---\n\n## 6. Payments and reconciliation\n\nTwo halves, and both are needed:\n\n- **`payments/statement`** — derived from the orders. What the period came to: cash our\n  captains collected for the shop, the delivery fees they owe us, the net. It always returns\n  an `assumptions` array, and a screen must show it — the most important entry says that a\n  delivered cash order is *assumed* to have yielded the amount due, because nothing records a\n  captain handing over a different figure.\n- **`payments/settlements`** — what was actually paid. Recorded by us; drafts are not shown.\n\n`settled_to_date` on the statement is the bridge between them.\n\n---\n\n## 7. Response envelope\n\nEvery response, success or failure, is the application's standard envelope:\n\n```json\n{\n  \"Model\": {},\n  \"Status\": true,\n  \"Message\": \"…\",\n  \"MessageDebug\": null,\n  \"Total\": 0,\n  \"Page\": 1,\n  \"Records\": 0\n}\n```\n\nBranch on `MessageDebug`'s key, not on the message text — the text is translated, the key is\nthe contract. `access_revoked`, `store_not_approved`, `owner_only`,\n`password_confirmation_failed`, `cannot_disable_self`, `webhook_url_missing`,\n`settlement_not_editable`.",
        "version": "1.0.0"
    },
    "servers": [
        {
            "url": "https://captain.kapitano.shop",
            "description": "API server"
        }
    ],
    "paths": {
        "/api/store-portal/profile": {
            "get": {
                "tags": [
                    "Portal — account"
                ],
                "summary": "Who am I",
                "description": "The signed-in person with their store attached, so a screen can draw its header and its\nmenu from one call.\n\nRead `can_manage_integration` rather than re-deriving the role rules: if it is false,\nevery owner-only route will answer 403.",
                "operationId": "portalProfileShow",
                "responses": {
                    "200": {
                        "description": "The account.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalUser"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The account or its store has been turned off since the token was issued.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            },
            "put": {
                "tags": [
                    "Portal — account"
                ],
                "summary": "Change my name or phone number",
                "description": "Not the email: it is the sign-in identifier, and an editable one makes a stolen session\npermanent. Not the role either — a staff account that could promote itself would be an\nowner account.\n\nThe phone matters more than it looks: it is where recovery codes go.",
                "operationId": "portalProfileUpdate",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "name",
                                    "phone"
                                ],
                                "properties": {
                                    "name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "phone": {
                                        "type": "string",
                                        "maxLength": 20
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Saved.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalUser"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/profile/password": {
            "post": {
                "tags": [
                    "Portal — account"
                ],
                "summary": "Change my password",
                "description": "Your **other sessions are left alone**, deliberately — nothing here suggests the old\npassword leaked, and revoking the token this very request authenticated with would sign\nyou out of the screen you are typing on.\n\nA *recovery* does the opposite and kills every session, because that is what somebody\ndoes when they have lost control of the account.\n\nA wrong current password comes back as a validation error against that field, not as a\n401: it is the expected mistake of this screen.",
                "operationId": "portalChangePassword",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "current_password",
                                    "password",
                                    "password_confirmation"
                                ],
                                "properties": {
                                    "current_password": {
                                        "type": "string"
                                    },
                                    "password": {
                                        "description": "Letters and numbers, and different from the current one.",
                                        "type": "string",
                                        "minLength": 10
                                    },
                                    "password_confirmation": {
                                        "type": "string"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Changed."
                    },
                    "422": {
                        "description": "The current password is wrong, or the new one is too weak.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/auth/register": {
            "post": {
                "tags": [
                    "Portal — auth"
                ],
                "summary": "Apply to integrate",
                "description": "Creates an **application**, not an integration: the store is `pending` and inactive, and\nno API token exists. Nothing can flow until an administrator approves it.\n\nNo token comes back either. Sign in afterwards like anybody else — handing back a\ncredential here would imply the shop is live when it is not.\n\n`webhook_url` is optional because a shop that has not built its receiver yet should still\nbe able to apply. When it is sent it goes through the same check as the settings screen:\nhttps, a public host, no credentials in the URL, standard port.",
                "operationId": "portalRegister",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "store_name",
                                    "name",
                                    "email",
                                    "phone",
                                    "password",
                                    "password_confirmation"
                                ],
                                "properties": {
                                    "store_name": {
                                        "type": "string",
                                        "example": "Fresh Market",
                                        "maxLength": 255
                                    },
                                    "name": {
                                        "description": "The person applying. They become the owner.",
                                        "type": "string",
                                        "example": "Layla Q",
                                        "maxLength": 255
                                    },
                                    "email": {
                                        "description": "The sign-in identifier. Unique across every store.",
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "phone": {
                                        "description": "Where recovery codes are sent. Make sure it is reachable.",
                                        "type": "string",
                                        "example": "966500000001",
                                        "maxLength": 20
                                    },
                                    "password": {
                                        "description": "At least 10 characters, with letters and numbers.",
                                        "type": "string",
                                        "minLength": 10
                                    },
                                    "password_confirmation": {
                                        "type": "string"
                                    },
                                    "webhook_url": {
                                        "type": "string",
                                        "example": "https://api.freshmarket.test/kapitano/webhooks",
                                        "nullable": true
                                    },
                                    "default_currency": {
                                        "type": "string",
                                        "example": "SAR",
                                        "nullable": true,
                                        "maxLength": 3
                                    },
                                    "timezone": {
                                        "type": "string",
                                        "example": "Asia/Riyadh",
                                        "nullable": true
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Application received.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalUser"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed — including a callback URL that is not public.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Too many attempts. Throttled per email and per address."
                    }
                }
            }
        },
        "/api/store-portal/auth/login": {
            "post": {
                "tags": [
                    "Portal — auth"
                ],
                "summary": "Sign in",
                "description": "Returns a bearer token and the signed-in person, with their store attached — so a screen\ndoes not have to call `profile` immediately afterwards to know what to draw.\n\nAn owner whose store is **still under review can sign in**. They reach `settings/*` and\nnothing else; see the API description.\n\nAn unknown address and a wrong password answer **identically**. Anything else would turn\nthis endpoint into a directory of which shops deliver with us.",
                "operationId": "portalLogin",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "email",
                                    "password"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "password": {
                                        "type": "string"
                                    },
                                    "device_name": {
                                        "type": "string",
                                        "nullable": true
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Signed in.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "properties": {
                                        "Model": {
                                            "properties": {
                                                "token": {
                                                    "type": "string"
                                                },
                                                "user": {
                                                    "$ref": "#/components/schemas/PortalUser"
                                                }
                                            },
                                            "type": "object"
                                        },
                                        "Status": {
                                            "type": "boolean",
                                            "example": true
                                        }
                                    },
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Wrong credentials, or an address nobody holds.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The account is suspended, or the store was rejected.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Too many attempts."
                    }
                }
            }
        },
        "/api/store-portal/auth/forgot-password": {
            "post": {
                "tags": [
                    "Portal — auth"
                ],
                "summary": "Send a recovery code",
                "description": "Sends a code to the phone number on the account.\n\nAnswers `200` for an address nobody holds, and sends nothing — same reasoning as sign-in.\n\nThis is also how an **invited colleague sets their first password**: they are created\nwithout one, so their first act is to ask for a code here.",
                "operationId": "portalForgotPassword",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "email"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "A code has been sent, if that address has an account."
                    },
                    "429": {
                        "description": "Too many attempts."
                    }
                }
            }
        },
        "/api/store-portal/auth/resend-code": {
            "post": {
                "tags": [
                    "Portal — auth"
                ],
                "summary": "Send the recovery code again",
                "description": "For somebody who never received the first one.\n\nIt **replaces** the pending code rather than adding a second — only the newest is\naccepted, so a person working through two messages does not meet a confusing refusal.\n\nThrottled per address and per IP, because each one costs money to send.",
                "operationId": "portalResendCode",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "email"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Sent, if that address has an account."
                    },
                    "429": {
                        "description": "Too many attempts."
                    }
                }
            }
        },
        "/api/store-portal/auth/reset-password": {
            "post": {
                "tags": [
                    "Portal — auth"
                ],
                "summary": "Set a new password with the code",
                "description": "**Every other session is revoked.** A recovery is what somebody does when they have lost\ncontrol of an account, so anything already signed in with it should stop working — unlike\nan ordinary password change from `profile/password`, which leaves other sessions alone.",
                "operationId": "portalResetPassword",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "email",
                                    "code",
                                    "password",
                                    "password_confirmation"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "code": {
                                        "type": "string",
                                        "example": "481920"
                                    },
                                    "password": {
                                        "type": "string",
                                        "minLength": 10
                                    },
                                    "password_confirmation": {
                                        "type": "string"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Password changed."
                    },
                    "422": {
                        "description": "The code was wrong, expired or already used.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/api/store-portal/auth/logout": {
            "post": {
                "tags": [
                    "Portal — auth"
                ],
                "summary": "Sign out of every device",
                "operationId": "portalLogout",
                "responses": {
                    "200": {
                        "description": "Tokens revoked."
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/orders": {
            "get": {
                "tags": [
                    "Portal — orders"
                ],
                "summary": "This store's orders",
                "description": "Only ever this store's own. The tenant clause is applied before every filter below, so\neven a `search` that exactly matches another shop's order number returns nothing.\n\nThe order shape is the same one your servers are sent by webhook, so a single parser\nserves both.",
                "operationId": "portalOrdersIndex",
                "parameters": [
                    {
                        "name": "rows",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "maximum": 25
                        },
                        "example": 25
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        },
                        "example": 1
                    },
                    {
                        "name": "search",
                        "in": "query",
                        "description": "Our order number, your reference, or the customer name.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "pending",
                                "assigned",
                                "picked_up",
                                "on_the_way",
                                "delivered",
                                "delivery_failed",
                                "cancelled"
                            ]
                        }
                    },
                    {
                        "name": "payment_status",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "paid",
                                "unpaid",
                                "refunded"
                            ]
                        }
                    },
                    {
                        "name": "external_order_id",
                        "in": "query",
                        "description": "Your own reference, exactly.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "from",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    },
                    {
                        "name": "to",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of orders.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalOrder"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The store is not approved yet.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/orders/{order}": {
            "get": {
                "tags": [
                    "Portal — orders"
                ],
                "summary": "One order, with its timeline",
                "description": "The `timeline` is the same set of transitions you are sent one at a time by webhook,\ngathered into a list. It carries **no actor**: which of our staff or which captain moved\nan order is our operational record.\n\nAn order belonging to another store answers **404**, not 403.",
                "operationId": "portalOrderShow",
                "parameters": [
                    {
                        "name": "order",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The order.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalOrder"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No such order of yours.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/webhooks": {
            "get": {
                "tags": [
                    "Portal — orders"
                ],
                "summary": "What we have tried to tell you",
                "description": "The answer to \"did you ever send it?\" — every callback we queued for this store, with the\nresponse we got and the error if there was one.\n\n`Summary` counts your own deliveries by status. `pending` piling up means a delivery\nworker is not running on our side; `dropped` means we gave up after six attempts and\nsomebody has to look.",
                "operationId": "portalWebhooksIndex",
                "parameters": [
                    {
                        "name": "rows",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "maximum": 25
                        },
                        "example": 25
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        },
                        "example": 1
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "pending",
                                "sent",
                                "failed",
                                "dropped"
                            ]
                        }
                    },
                    {
                        "name": "event",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        },
                        "example": "order.delivered"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of deliveries.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalWebhookDelivery"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/webhooks/{delivery}/replay": {
            "post": {
                "tags": [
                    "Portal — orders"
                ],
                "summary": "Send a callback again",
                "description": "Resets the attempt count and queues it afresh. Owner only.\n\nIt can only ever go to the URL already on your own record, so this cannot be aimed\nanywhere — but your handler will see the event a second time, which is what `event_id` is\nfor.",
                "operationId": "portalWebhookReplay",
                "parameters": [
                    {
                        "name": "delivery",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Queued again.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalWebhookDelivery"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not one of yours.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/payments/statement": {
            "get": {
                "tags": [
                    "Portal — payments"
                ],
                "summary": "What the period came to",
                "description": "Derived from your delivered orders. Defaults to the last thirty days. A range entered\nbackwards is swapped rather than answered empty.\n\n**Read `assumptions` and show it.** The important one: there is no record of a captain\nhanding over an amount different from the one due, so a delivered cash order is *assumed*\nto have yielded exactly `amount_to_collect`. Quoting a cash figure in a dispute without\nsaying that is how an argument starts.\n\n`totals` is **one row per currency** and is never summed across them. `outcomes` counts\nfailed and cancelled orders beside the money rather than netting them into it.\n`settled_to_date` is what we have actually paid you — the bridge to `payments/settlements`.",
                "operationId": "portalStatement",
                "parameters": [
                    {
                        "name": "from",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    },
                    {
                        "name": "to",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The statement.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalStatement"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/payments/orders": {
            "get": {
                "tags": [
                    "Portal — payments"
                ],
                "summary": "The orders behind a total",
                "description": "So a disputed figure resolves to the lines that produced it.",
                "operationId": "portalStatementLines",
                "parameters": [
                    {
                        "name": "rows",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "maximum": 25
                        },
                        "example": 25
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        },
                        "example": 1
                    },
                    {
                        "name": "from",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    },
                    {
                        "name": "to",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of delivered orders.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalOrder"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/payments/settlements": {
            "get": {
                "tags": [
                    "Portal — payments"
                ],
                "summary": "What we have actually paid you",
                "description": "The other half of a reconciliation. The statement says what a period came to; these are\nthe payments that happened, and the gap between the two is the conversation.\n\nSettlements still being drawn up are **not shown** — a figure nobody has stood behind yet\nis not one worth arguing about. One that is not yours, or is still a draft, answers 404.",
                "operationId": "portalSettlements",
                "parameters": [
                    {
                        "name": "rows",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "maximum": 25
                        },
                        "example": 25
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        },
                        "example": 1
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of settlements.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalSettlement"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/webhooks/{delivery}": {
            "get": {
                "tags": [
                    "Portal — orders"
                ],
                "summary": "One delivery, with the bytes we sent",
                "description": "The payload we sent, what your endpoint answered, and the error if there was one.\n\nThis is the thing to quote when you tell us a callback never arrived — it settles the\nquestion in one direction or the other rather than starting a search.\n\nOne that is not yours answers **404**.",
                "operationId": "portalWebhookShow",
                "parameters": [
                    {
                        "name": "delivery",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The delivery.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalWebhookDelivery"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not one of yours.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/payments/settlements/{settlement}": {
            "get": {
                "tags": [
                    "Portal — payments"
                ],
                "summary": "One payment",
                "description": "A settlement still being drawn up answers **404**, exactly like one belonging to another\nstore — a figure nobody has stood behind yet is not one worth showing, and a 403 would\ntell you it exists.",
                "operationId": "portalSettlementShow",
                "parameters": [
                    {
                        "name": "settlement",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The settlement.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalSettlement"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not yours, or still a draft.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/settings": {
            "get": {
                "tags": [
                    "Portal — settings"
                ],
                "summary": "The store record and its integration settings",
                "description": "Returns **metadata about the secrets, never the secrets themselves**: whether they exist,\nwhen they were last rotated, and when the previous pair stops being accepted. Use\n`settings/secrets/reveal` for the values.",
                "operationId": "portalSettingsShow",
                "responses": {
                    "200": {
                        "description": "The store.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalStore"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "A staff account reached an owner-only route.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/settings/webhook": {
            "put": {
                "tags": [
                    "Portal — settings"
                ],
                "summary": "Set where callbacks are delivered",
                "description": "The URL must be **https**, on a **public** host, with the standard port and no credentials\nin it.\n\nThat is not fussiness. We make the request, from inside our own network, to whatever is\ntyped — so a URL such as `https://169.254.169.254/...` would turn this field into a reader\nfor our server's cloud credentials. A name is also re-checked at the moment each callback\nis sent, because a host that resolves publicly today can resolve privately tomorrow.\n\nFollow it with `settings/webhook/test` rather than waiting for a real order.",
                "operationId": "portalUpdateWebhook",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "webhook_url"
                                ],
                                "properties": {
                                    "webhook_url": {
                                        "type": "string",
                                        "example": "https://api.freshmarket.test/kapitano/webhooks"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Saved.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalStore"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Not https, not public, or otherwise refused.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/settings/ips": {
            "put": {
                "tags": [
                    "Portal — settings"
                ],
                "summary": "Restrict which addresses may call the integration API",
                "description": "An **empty array switches the check off**, which is how onboarding runs.\n\nThis gates the machine API, not the portal — so getting it wrong locks your servers out\nwhile leaving you able to sign in here and correct it.",
                "operationId": "portalUpdateAllowedIps",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "allowed_ips"
                                ],
                                "properties": {
                                    "allowed_ips": {
                                        "type": "array",
                                        "items": {
                                            "type": "string",
                                            "format": "ipv4"
                                        },
                                        "example": [
                                            "203.0.113.10"
                                        ],
                                        "maxItems": 20
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Saved.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalStore"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Not a valid address.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/settings/secrets/reveal": {
            "post": {
                "tags": [
                    "Portal — settings"
                ],
                "summary": "Show the signing secrets",
                "description": "Requires the caller's **password again**. A stolen session must not be worth as much as a\nstolen signing key, and re-authentication is the cheapest thing that keeps them apart.\n\nThe response carries `Cache-Control: no-store`. Do not log it, and do not put it in\nbrowser storage.\n\n- `signing_secret` — what **you** sign requests to us with.\n- `webhook_secret` — what **we** sign callbacks to you with.",
                "operationId": "portalRevealSecrets",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "password"
                                ],
                                "properties": {
                                    "password": {
                                        "type": "string"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The two secrets.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "properties": {
                                        "Model": {
                                            "properties": {
                                                "signing_secret": {
                                                    "type": "string",
                                                    "example": "whsec_…"
                                                },
                                                "webhook_secret": {
                                                    "type": "string",
                                                    "example": "whsec_…"
                                                }
                                            },
                                            "type": "object"
                                        }
                                    },
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Wrong password, or not the owner.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/settings/secrets/rotate": {
            "post": {
                "tags": [
                    "Portal — settings"
                ],
                "summary": "Issue a new pair of secrets",
                "description": "**This does not cause an outage.** For 24 hours the previous pair keeps working:\n\n- inbound, we accept a request signed with either secret;\n- outbound, every callback is signed with **both**, and the signature header carries two\n  `v1` values.\n\nSo deploy the new secret whenever you like inside that window. Your verifier must iterate\nthe `v1` entries rather than reading the first one — if it does not, callbacks will start\nfailing the moment you press this.\n\nRequires the password again, and the response is `no-store`.",
                "operationId": "portalRotateSecrets",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "password"
                                ],
                                "properties": {
                                    "password": {
                                        "type": "string"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The new pair.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "properties": {
                                        "Model": {
                                            "properties": {
                                                "signing_secret": {
                                                    "type": "string"
                                                },
                                                "webhook_secret": {
                                                    "type": "string"
                                                }
                                            },
                                            "type": "object"
                                        }
                                    },
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Wrong password, or not the owner.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/settings/webhook/test": {
            "post": {
                "tags": [
                    "Portal — settings"
                ],
                "summary": "Send a test callback",
                "description": "Queues a real `integration.ping` to the configured URL — the same queue, the same\nsignature and the same delivery ledger a real event uses. A test that took a shortcut\nwould pass while the real path was broken.\n\nIt carries **no `order` and no `sequence`**, so tolerate both being absent in your\nhandler. Watch the result in `GET /webhooks`.",
                "operationId": "portalTestWebhook",
                "responses": {
                    "200": {
                        "description": "Queued. The delivery row is returned.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalWebhookDelivery"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "No callback URL is set yet.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/staff": {
            "get": {
                "tags": [
                    "Portal — staff"
                ],
                "summary": "The people at this store",
                "operationId": "portalStaffIndex",
                "parameters": [
                    {
                        "name": "rows",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "maximum": 25
                        },
                        "example": 25
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "integer",
                            "minimum": 1
                        },
                        "example": 1
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of people.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalUser"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            },
            "post": {
                "tags": [
                    "Portal — staff"
                ],
                "summary": "Add a colleague",
                "description": "No password is set here, by you or by us — the new account has one nobody knows. The\nperson signs in for the first time by asking for a recovery code on the sign-in screen,\nwhich proves they hold the phone number you entered. **Enter it carefully.**\n\nThe role is always `staff`. An owner cannot mint another owner: that is how a removed\nemployee keeps their access.",
                "operationId": "portalStaffInvite",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "name",
                                    "email",
                                    "phone"
                                ],
                                "properties": {
                                    "name": {
                                        "type": "string"
                                    },
                                    "email": {
                                        "description": "Unique across every store.",
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "phone": {
                                        "description": "Where their first recovery code goes.",
                                        "type": "string"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Added.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalUser"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "That address already has an account.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        },
        "/api/store-portal/staff/{member}/activation": {
            "patch": {
                "tags": [
                    "Portal — staff"
                ],
                "summary": "Turn a colleague's access on or off",
                "description": "Turning somebody off revokes their sessions immediately rather than waiting for a token\nto expire.\n\nYou cannot do this to **yourself**: the owner is the only role that can change the\nintegration, so locking yourself out would need somebody else to undo.",
                "operationId": "portalStaffActivation",
                "parameters": [
                    {
                        "name": "member",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "required": [
                                    "is_active"
                                ],
                                "properties": {
                                    "is_active": {
                                        "type": "boolean"
                                    }
                                },
                                "type": "object"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Saved.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalUser"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not somebody at this store.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "You cannot turn off your own account.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PortalError"
                                }
                            }
                        }
                    }
                },
                "security": [
                    {
                        "portalToken": []
                    }
                ]
            }
        }
    },
    "components": {
        "schemas": {
            "PortalError": {
                "properties": {
                    "Model": {
                        "description": "The refusal envelope.\n\nBranch on the key inside `MessageDebug`, never on `Message` — the message is translated to\nwhatever `Accept-Language` asked for, the key is the contract.",
                        "nullable": true
                    },
                    "Status": {
                        "type": "boolean",
                        "example": false
                    },
                    "Message": {
                        "description": "Translated. For people, not for code.",
                        "type": "string"
                    },
                    "MessageDebug": {
                        "description": "One key naming what went wrong: `access_revoked`, `store_not_approved`, `owner_only`, `password_confirmation_failed`, `cannot_disable_self`, `webhook_url_missing`, `settlement_not_editable`, `invalid_settlement_transition`.",
                        "type": "object",
                        "example": {
                            "store_not_approved": []
                        },
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "PortalOrder": {
                "properties": {
                    "uuid": {
                        "description": "Our identifier. Use it on every later call.",
                        "type": "string",
                        "format": "uuid"
                    },
                    "external_order_id": {
                        "description": "Your own reference, as you sent it.",
                        "type": "string",
                        "nullable": true
                    },
                    "order_number": {
                        "type": "string",
                        "example": "ORD-000482"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "assigned",
                            "picked_up",
                            "on_the_way",
                            "delivered",
                            "delivery_failed",
                            "cancelled"
                        ]
                    },
                    "status_label": {
                        "type": "string"
                    },
                    "captain": {
                        "properties": {
                            "name": {
                                "type": "string"
                            },
                            "phone": {
                                "type": "string"
                            }
                        },
                        "type": "object",
                        "nullable": true
                    },
                    "eta_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "promised_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "timestamps": {
                        "description": "received_at, picked_up_at, on_the_way_at, delivered_at, failed_at, cancelled_at.",
                        "type": "object"
                    },
                    "cash": {
                        "properties": {
                            "method": {
                                "type": "string",
                                "enum": [
                                    "cash_on_delivery",
                                    "prepaid"
                                ]
                            },
                            "status": {
                                "type": "string",
                                "nullable": true,
                                "enum": [
                                    "paid",
                                    "unpaid",
                                    "refunded"
                                ]
                            },
                            "amount_to_collect": {
                                "type": "number",
                                "nullable": true
                            },
                            "currency": {
                                "type": "string"
                            }
                        },
                        "type": "object"
                    },
                    "failure_reason": {
                        "type": "string",
                        "nullable": true
                    },
                    "cancel_reason": {
                        "type": "string",
                        "nullable": true
                    },
                    "timeline": {
                        "description": "Only on the detail endpoint. No actor: which of our people moved the order is our record.",
                        "type": "array",
                        "items": {
                            "properties": {
                                "status": {
                                    "type": "string"
                                },
                                "status_label": {
                                    "type": "string"
                                },
                                "note": {
                                    "type": "string",
                                    "nullable": true
                                },
                                "at": {
                                    "type": "string",
                                    "format": "date-time"
                                }
                            },
                            "type": "object"
                        }
                    }
                },
                "type": "object"
            },
            "PortalSettlement": {
                "properties": {
                    "uuid": {
                        "description": "One payment between us and the store.\n\nThe amounts are a snapshot of what both sides agreed on the day, not a live view — they do not\nmove if an order is corrected afterwards.",
                        "type": "string",
                        "format": "uuid"
                    },
                    "reference": {
                        "description": "The bank or transfer reference.",
                        "type": "string",
                        "nullable": true
                    },
                    "period": {
                        "properties": {
                            "from": {
                                "type": "string",
                                "format": "date"
                            },
                            "to": {
                                "type": "string",
                                "format": "date"
                            }
                        },
                        "type": "object"
                    },
                    "cash_collected": {
                        "type": "number"
                    },
                    "fees_charged": {
                        "type": "number"
                    },
                    "net_amount": {
                        "description": "What was actually paid. May be negative when fees exceeded the cash collected.",
                        "type": "number"
                    },
                    "currency": {
                        "type": "string"
                    },
                    "orders_count": {
                        "type": "integer"
                    },
                    "status": {
                        "description": "Drafts are never shown to the store.",
                        "type": "string",
                        "enum": [
                            "settled",
                            "cancelled"
                        ]
                    },
                    "status_label": {
                        "type": "string"
                    },
                    "settled_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "note": {
                        "type": "string",
                        "nullable": true
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "PortalStatement": {
                "properties": {
                    "period": {
                        "properties": {
                            "from": {
                                "description": "What a period came to, and what has been paid against it.",
                                "type": "string",
                                "format": "date"
                            },
                            "to": {
                                "type": "string",
                                "format": "date"
                            }
                        },
                        "type": "object"
                    },
                    "totals": {
                        "description": "One row per currency. Never summed across them.",
                        "type": "array",
                        "items": {
                            "properties": {
                                "currency": {
                                    "type": "string",
                                    "example": "SAR"
                                },
                                "delivered_count": {
                                    "type": "integer"
                                },
                                "cash_collected": {
                                    "description": "Collected by our captains on your behalf.",
                                    "type": "number"
                                },
                                "prepaid_value": {
                                    "description": "Delivered value that was already paid for. No money moved through us.",
                                    "type": "number"
                                },
                                "fees_charged": {
                                    "description": "Our delivery fee, earned on every delivery however it was paid for.",
                                    "type": "number"
                                },
                                "net_due_to_store": {
                                    "description": "cash_collected − fees_charged.",
                                    "type": "number"
                                }
                            },
                            "type": "object"
                        }
                    },
                    "daily": {
                        "description": "The same figures per delivery day.",
                        "type": "array",
                        "items": {
                            "type": "object"
                        }
                    },
                    "outcomes": {
                        "description": "Order counts by final status. Failed and cancelled are counted here rather than netted off the money.",
                        "type": "object"
                    },
                    "assumptions": {
                        "description": "Show these with the figures. They say what the numbers rest on — most importantly that a delivered cash order is assumed to have yielded the amount due, because a different amount handed over at the door is not recorded anywhere.",
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "settled_to_date": {
                        "description": "Net actually paid, per currency, across every settled settlement. The bridge to `payments/settlements`.",
                        "type": "object",
                        "example": {
                            "SAR": 3900
                        }
                    }
                },
                "type": "object"
            },
            "PortalStore": {
                "properties": {
                    "uuid": {
                        "description": "The store's own record.\n\nNo secret appears in this shape anywhere, deliberately — only whether they exist and when\nthey were last changed.",
                        "type": "string",
                        "format": "uuid"
                    },
                    "name": {
                        "type": "string"
                    },
                    "slug": {
                        "description": "Our handle for you. Derived from the name; not editable.",
                        "type": "string"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "approved",
                            "rejected",
                            "suspended"
                        ]
                    },
                    "status_label": {
                        "type": "string"
                    },
                    "is_active": {
                        "description": "The operational pause switch, separate from the review status.",
                        "type": "boolean"
                    },
                    "is_live": {
                        "description": "Approved **and** not paused. Only then do orders flow.",
                        "type": "boolean"
                    },
                    "review_note": {
                        "description": "Why a decision went the way it did.",
                        "type": "string",
                        "nullable": true
                    },
                    "reviewed_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "webhook_url": {
                        "type": "string",
                        "nullable": true
                    },
                    "allowed_ips": {
                        "description": "Empty means the check is off.",
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "default_currency": {
                        "type": "string",
                        "example": "SAR"
                    },
                    "timezone": {
                        "type": "string",
                        "example": "Asia/Riyadh"
                    },
                    "secrets": {
                        "properties": {
                            "has_secrets": {
                                "type": "boolean"
                            },
                            "rotated_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                            },
                            "previous_valid_until": {
                                "description": "While this is set, the previous pair is still accepted and callbacks carry two `v1` values.",
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                            }
                        },
                        "type": "object"
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "PortalUser": {
                "properties": {
                    "uuid": {
                        "description": "A person at a store.",
                        "type": "string",
                        "format": "uuid"
                    },
                    "name": {
                        "type": "string"
                    },
                    "email": {
                        "type": "string",
                        "format": "email"
                    },
                    "phone": {
                        "type": "string"
                    },
                    "role": {
                        "type": "string",
                        "enum": [
                            "owner",
                            "staff"
                        ]
                    },
                    "role_label": {
                        "description": "Translated, ready to display.",
                        "type": "string"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "active",
                            "suspended"
                        ]
                    },
                    "status_label": {
                        "type": "string"
                    },
                    "is_active": {
                        "type": "boolean"
                    },
                    "can_manage_integration": {
                        "description": "Answered here so a screen decides what to draw from one flag rather than re-deriving the role rules. If this is false, every owner-only route will answer 403.",
                        "type": "boolean"
                    },
                    "last_login_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "store": {
                        "oneOf": [
                            {
                                "$ref": "#/components/schemas/PortalStore"
                            }
                        ],
                        "nullable": true,
                        "description": "Present on sign-in and on `profile`."
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                },
                "type": "object"
            },
            "PortalWebhookDelivery": {
                "properties": {
                    "uuid": {
                        "description": "One attempt to tell the store something.",
                        "type": "string",
                        "format": "uuid"
                    },
                    "event": {
                        "type": "string",
                        "example": "order.delivered"
                    },
                    "event_id": {
                        "description": "Your deduplication key. The same event retried carries the same one.",
                        "type": "string",
                        "format": "uuid"
                    },
                    "sequence": {
                        "description": "Increases per order. Discard anything not newer than what you hold. Null on `integration.ping`.",
                        "type": "integer",
                        "nullable": true
                    },
                    "status": {
                        "description": "`dropped` means we gave up after six attempts.",
                        "type": "string",
                        "enum": [
                            "pending",
                            "sent",
                            "failed",
                            "dropped"
                        ]
                    },
                    "attempts": {
                        "type": "integer"
                    },
                    "response_status": {
                        "description": "What your endpoint answered.",
                        "type": "integer",
                        "nullable": true
                    },
                    "error": {
                        "type": "string",
                        "nullable": true
                    },
                    "next_attempt_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "delivered_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                },
                "type": "object"
            }
        },
        "securitySchemes": {
            "portalToken": {
                "type": "http",
                "description": "The token returned by `POST /api/store-portal/auth/login`.",
                "scheme": "bearer"
            }
        }
    },
    "tags": [
        {
            "name": "Portal — auth",
            "description": "Registering, signing in, and recovering a password"
        },
        {
            "name": "Portal — account",
            "description": "The signed-in person's own record"
        },
        {
            "name": "Portal — settings",
            "description": "The integration: callback URL, allowed addresses, secrets"
        },
        {
            "name": "Portal — staff",
            "description": "The owner's colleagues"
        },
        {
            "name": "Portal — orders",
            "description": "The shop's own orders, their timelines and the delivery log"
        },
        {
            "name": "Portal — payments",
            "description": "The statement, and what has actually been paid"
        }
    ]
}