{
	"openapi": "3.0.3",
	"info": {
		"title": "Digital Savings — Portal API",
		"version": "1.0.0",
		"description": "The portal and ops surface for **Bomba Cash**: the savings product catalogue, the hourly liability tally, and the one action an operator can take on a stuck payment order.\n\nEvery answer carries the same envelope — `data` on a success, `error` on a failure, never both — so a caller branches on one field rather than on the status.\n\nEvery route but one is behind a Cognito user pool. Ask `POST /auth/token` for a token, sending the email address or phone number you sign in with and your password, then send it as `Authorization: Bearer <accessToken>`. A token lasts an hour. A request without one is answered `401 Unauthorized` by the gateway, before any of this API runs.\n\nTo use **Try it out**, call `POST /auth/token` first and paste the `accessToken` it returns into **Authorize**. The Postman collection does the same thing with an `{{accessToken}}` variable."
	},
	"servers": [
		{
			"url": "https://portal-api.internal.savings.kamoa.io",
			"description": "internal — signed with SigV4."
		}
	],
	"security": [
		{
			"bearerAuth": []
		}
	],
	"paths": {
		"/roles": {
			"get": {
				"operationId": "listPortalRoles",
				"summary": "Every role and what it grants. With pairs=1, one row per organisation and role that has holders you can see, and one with no organisation for a role none of them holds.",
				"parameters": [
					{
						"name": "pairs",
						"in": "query",
						"required": false,
						"description": "1 for the organisation and role pairs",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					}
				}
			},
			"post": {
				"operationId": "createPortalRole",
				"summary": "Create a role. Its key is built from the name, and it may grant only what your roles grant.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreatePortalRoleBody"
							},
							"example": {
								"name": "Operations view",
								"description": "Reads the Operations section and nothing else.",
								"pageGrants": {
									"savings": [
										"read"
									],
									"maturities": [
										"read"
									]
								},
								"fieldGrants": {}
							}
						}
					}
				},
				"responses": {
					"201": {
						"description": "The role was created.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "A role of that name exists.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/roles/{roleKey}": {
			"get": {
				"operationId": "getPortalRole",
				"summary": "One role and what it grants.",
				"parameters": [
					{
						"name": "roleKey",
						"in": "path",
						"required": true,
						"description": "The role, by its group name, e.g. role:admin",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"404": {
						"description": "No role by that key.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			},
			"patch": {
				"operationId": "updatePortalRole",
				"summary": "Change a role's name, description or grants. It may grant only what your roles grant, and may not leave you unable to edit roles.",
				"parameters": [
					{
						"name": "roleKey",
						"in": "path",
						"required": true,
						"description": "The role, by its group name, e.g. role:admin",
						"schema": {
							"type": "string"
						}
					}
				],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/UpdatePortalRoleBody"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "No role by that key.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			},
			"delete": {
				"operationId": "retirePortalRole",
				"summary": "Retire a role nobody holds. Super only, and never a role the pool ships with.",
				"parameters": [
					{
						"name": "roleKey",
						"in": "path",
						"required": true,
						"description": "The role, by its group name, e.g. role:finance",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The retired role, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "No role by that key.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "Someone holds it, or the pool ships with it.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/roles/{roleKey}/holders": {
			"get": {
				"operationId": "listPortalRoleHolders",
				"summary": "Who holds a role, among the users you can see.",
				"parameters": [
					{
						"name": "roleKey",
						"in": "path",
						"required": true,
						"description": "The role, by its group name, e.g. role:admin",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"404": {
						"description": "No role by that key.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/organisations": {
			"get": {
				"operationId": "listPortalOrganisations",
				"summary": "The organisations you hold, and who each is.",
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					}
				}
			}
		},
		"/organisations/{id}": {
			"patch": {
				"operationId": "updatePortalOrganisation",
				"summary": "Change an organisation's contact details, or which pages its people may reach. Only one you hold, and the pages only as a super.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "The organisation key, e.g. bch",
						"schema": {
							"type": "string"
						}
					}
				],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/UpdatePortalOrganisationBody"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "No organisation by that key among those you hold.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/activity": {
			"get": {
				"operationId": "listPortalActivity",
				"summary": "What changed, newest first, among the changes that concern an organisation you hold.",
				"parameters": [
					{
						"name": "actor",
						"in": "query",
						"required": false,
						"description": "One person, by their subject",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"description": "Rows, roughly, 1 to 100",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"description": "The `nextCursor` of the page before",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "A page under `data`, with a `nextCursor` when more may follow.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/products/{code}/events": {
			"get": {
				"operationId": "listProductEvents",
				"summary": "A product's change history, newest first.",
				"parameters": [
					{
						"name": "code",
						"in": "path",
						"required": true,
						"description": "The product code, e.g. prod3m",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					}
				}
			}
		},
		"/users/{id}/events": {
			"get": {
				"operationId": "listPortalUserEvents",
				"summary": "A user's change history, newest first. Only a user you can see.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "The user's subject",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"404": {
						"description": "No user on that subject, or one you may not see. The two answer alike.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/roles/{roleKey}/events": {
			"get": {
				"operationId": "listPortalRoleEvents",
				"summary": "A role's change history, newest first.",
				"parameters": [
					{
						"name": "roleKey",
						"in": "path",
						"required": true,
						"description": "The role, by its group name, e.g. role:admin",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					}
				}
			}
		},
		"/organisations/{id}/events": {
			"get": {
				"operationId": "listPortalOrganisationEvents",
				"summary": "An organisation's change history, newest first. Only one you hold.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "The organisation key, e.g. bch",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"404": {
						"description": "No organisation by that key among those you hold.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/settings": {
			"get": {
				"operationId": "getPortalSettings",
				"summary": "The platform settings: SMS on or off, the terms link, and when each scheduled worker runs.",
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					}
				}
			},
			"patch": {
				"operationId": "updatePortalSettings",
				"summary": "Change any setting. A schedule is written to its worker's Rule, and every value changed is on the trail.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/UpdatePortalSettingsBody"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "Someone else changed the settings since this request read them. Nothing was written.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/settings/reference": {
			"get": {
				"operationId": "getPortalSettingsReference",
				"summary": "What the deployed code holds: partners, their currencies, the SMS categories and the scheduled workers.",
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					}
				}
			}
		},
		"/products": {
			"get": {
				"operationId": "listProducts",
				"summary": "One market's catalogue: each product's version in force and its next scheduled change.",
				"parameters": [
					{
						"name": "currency",
						"in": "query",
						"required": false,
						"description": "The market, e.g. XAF. Defaults to XAF",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			},
			"post": {
				"operationId": "createProductVersion",
				"summary": "Append one product version. Versions are immutable.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreateProductVersionBody"
							},
							"example": {
								"currency": "XAF",
								"accrualPeriod": "MONTH",
								"accrualPeriods": 3,
								"interestRate": "0.055",
								"discountRate": "0.10",
								"minAmount": "1000",
								"maxBalance": "2000000",
								"earlyExitHoldSeconds": 172800,
								"effectiveFrom": "2027-01-01T00:00:00.000Z",
								"status": "ACTIVE"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "That version id already existed. The terms sent were discarded and the stored row comes back.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"201": {
						"description": "The version was written.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "Another version of this product already takes effect at that instant.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/products/{code}/versions/{version}": {
			"delete": {
				"operationId": "cancelScheduledVersion",
				"summary": "Take back a version that has not taken effect. One in force is never removed.",
				"parameters": [
					{
						"name": "code",
						"in": "path",
						"required": true,
						"description": "The product code, e.g. prod3m",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "version",
						"in": "path",
						"required": true,
						"description": "The version, e.g. v3",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "currency",
						"in": "query",
						"required": true,
						"description": "The market the version prices, e.g. XAF",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "Nothing answers for that id — which is not the same as zero.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "The version has taken effect.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/auth/me": {
			"get": {
				"operationId": "getPortalMe",
				"summary": "Who you are: your alias, your name, the groups you hold, and the pages and actions your roles grant.",
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"404": {
						"description": "No user on that subject, or one you may not see. The two answer alike.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/auth/token": {
			"post": {
				"operationId": "createPortalToken",
				"summary": "Exchange an email address or phone number and a password for a token.",
				"security": [],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreatePortalTokenBody"
							},
							"example": {
								"username": "ops@kamoa.io",
								"password": "…"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "The token, under `data`. It lasts an hour.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"401": {
						"description": "Those credentials were refused. A wrong password and an unknown user answer the same way.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "The pool wants something more of this user before a token can be issued. `AUTH_NEW_PASSWORD_REQUIRED` and `AUTH_MFA_REQUIRED` say which, so a caller renders the right screen without reading the message; `AUTH_CHALLENGE` is anything else Cognito raised.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/products/{code}": {
			"get": {
				"operationId": "getProductInForce",
				"summary": "The version of one product in force, whatever its status.",
				"parameters": [
					{
						"name": "code",
						"in": "path",
						"required": true,
						"description": "The product code",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "asOf",
						"in": "query",
						"required": false,
						"description": "Read the catalogue as at this instant. Absent means now",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "Nothing answers for that id — which is not the same as zero.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/products/{code}/versions": {
			"get": {
				"operationId": "listProductVersions",
				"summary": "One page of a product's versions, oldest first.",
				"parameters": [
					{
						"name": "code",
						"in": "path",
						"required": true,
						"description": "The product code",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"description": "Rows per page, 1 to 100. Defaults to 50",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"description": "The `nextCursor` of the page before",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "A page under `data`, with `count` and a `nextCursor` when one follows.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "Nothing answers for that id — which is not the same as zero.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/liability/days": {
			"get": {
				"operationId": "getLiabilityRange",
				"summary": "What the book owed across a window, one row per day.",
				"parameters": [
					{
						"name": "from",
						"in": "query",
						"required": true,
						"description": "First day, YYYY-MM-DD",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "to",
						"in": "query",
						"required": true,
						"description": "Last day, YYYY-MM-DD",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One row per day under `data`. A day nothing valued carries `rows: 0`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/liability/days/{asOf}": {
			"get": {
				"operationId": "getLiabilityDay",
				"summary": "What the book owed on one day.",
				"parameters": [
					{
						"name": "asOf",
						"in": "path",
						"required": true,
						"description": "The day, YYYY-MM-DD",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "No run valued that day — which is not the same as a day the book owed nothing.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/liability/products/{code}/versions/{version}": {
			"get": {
				"operationId": "getProductLiabilitySeries",
				"summary": "One product version's liability over a window, oldest first.",
				"parameters": [
					{
						"name": "code",
						"in": "path",
						"required": true,
						"description": "The product code, e.g. prod3m",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "version",
						"in": "path",
						"required": true,
						"description": "The version, e.g. v2",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "currency",
						"in": "query",
						"required": false,
						"description": "The market, e.g. XAF. Defaults to XAF",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "from",
						"in": "query",
						"required": true,
						"description": "First day, YYYY-MM-DD",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "to",
						"in": "query",
						"required": true,
						"description": "Last day, YYYY-MM-DD",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The rows under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/funding": {
			"post": {
				"operationId": "fundPool",
				"summary": "Put the business's own money into the pooled wallet.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/FundPoolBody"
							},
							"example": {
								"reference": "treasury-2026-09-18-01",
								"currency": "XAF",
								"amount": "5000000"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "That reference had already been posted. The pool was not topped up again.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"201": {
						"description": "The top-up was posted to the ledger.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/orders/{reference}/redrive": {
			"post": {
				"operationId": "redriveStuckOrder",
				"summary": "Clear one stuck order's marker so the overdue sweep picks it up again.",
				"parameters": [
					{
						"name": "reference",
						"in": "path",
						"required": true,
						"description": "The payment order's reference",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The marker was cleared.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "That reference is not a stuck PENDING order.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/auth/challenge": {
			"post": {
				"operationId": "answerPortalChallenge",
				"summary": "Answer the challenge a sign-in was refused on, and get a token. Send the password again — the session is taken out here.",
				"security": [],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/AnswerPortalChallengeBody"
							},
							"example": {
								"username": "ops@kamoa.io",
								"password": "…",
								"newPassword": "…"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "The token, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"401": {
						"description": "Those credentials were refused. A wrong password and an unknown user answer the same way.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "The pool raised a further challenge, named by the same codes as `/auth/token` — MFA usually follows a first password.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/auth/forgot": {
			"post": {
				"operationId": "startPortalPasswordReset",
				"summary": "Send a reset code to the alias. An unknown alias answers the same way.",
				"security": [],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/StartPortalPasswordResetBody"
							},
							"example": {
								"username": "ops@kamoa.io"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "A code was sent, if there was anywhere to send it.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/auth/forgot/confirm": {
			"post": {
				"operationId": "confirmPortalPasswordReset",
				"summary": "Finish a reset with the code and a new password.",
				"security": [],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/ConfirmPortalPasswordResetBody"
							},
							"example": {
								"username": "ops@kamoa.io",
								"code": "123456",
								"password": "…"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "The password was set.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"401": {
						"description": "That code is wrong or has expired.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/auth/password": {
			"post": {
				"operationId": "changePortalPassword",
				"summary": "Change your own password while signed in.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/ChangePortalPasswordBody"
							},
							"example": {
								"previousPassword": "…",
								"proposedPassword": "…"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "The password was changed.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"401": {
						"description": "Those credentials were refused. A wrong password and an unknown user answer the same way.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/auth/mfa": {
			"post": {
				"operationId": "enablePortalMfa",
				"summary": "Turn TOTP on. With no code it answers the secret an authenticator app takes; with one it confirms and enables.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/EnablePortalMfaBody"
							},
							"example": {
								"code": "123456"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "`secretCode` when the enrolment started, `enabled` when it was confirmed.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"401": {
						"description": "Those credentials were refused. A wrong password and an unknown user answer the same way.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			},
			"delete": {
				"operationId": "disablePortalMfa",
				"summary": "Turn TOTP off.",
				"responses": {
					"200": {
						"description": "TOTP is off.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"401": {
						"description": "Those credentials were refused. A wrong password and an unknown user answer the same way.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/users": {
			"post": {
				"operationId": "createPortalUser",
				"summary": "Invite one user by the email or phone number they will sign in with, scoped to organisations and given roles.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreatePortalUserBody"
							},
							"example": {
								"name": "Awa Ndiaye",
								"email": "awa@kamoa.io",
								"organisations": [
									"kamoa"
								],
								"roles": [
									"role:operations",
									"role:tech"
								]
							}
						}
					}
				},
				"responses": {
					"201": {
						"description": "The user, under `data`, awaiting their first sign-in.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "That alias already signs in.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			},
			"get": {
				"operationId": "listPortalUsers",
				"summary": "Every user you share an organisation with, each with all the groups they hold. One response, never paged.",
				"responses": {
					"200": {
						"description": "The users under `data`, with `count`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/users/{id}": {
			"patch": {
				"operationId": "updatePortalUser",
				"summary": "Change a user's organisations or roles, add an alias they lack, or disable them.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "The user's subject",
						"schema": {
							"type": "string"
						}
					}
				],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/UpdatePortalUserBody"
							},
							"example": {
								"roles": [
									"role:support"
								],
								"enabled": false
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"400": {
						"description": "The request could not be read. `error.code` names which field.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "No user on that subject, or one you may not see. The two answer alike.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/users/{id}/reset": {
			"post": {
				"operationId": "resetPortalUserPassword",
				"summary": "Force a password reset. Their next sign-in is a challenge.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "The user's subject",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "No user on that subject, or one you may not see. The two answer alike.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/users/{id}/invite": {
			"post": {
				"operationId": "resendPortalUserInvite",
				"summary": "Send the invitation again, reissuing the temporary password.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "The user's subject",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "One record, under `data`.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object"
								}
							}
						}
					},
					"403": {
						"description": "That user, or something you tried to hand out, is outside your reach. You may only assign an organisation or a role you hold yourself.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "No user on that subject, or one you may not see. The two answer alike.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"409": {
						"description": "That user has already signed in — force a reset instead.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		}
	},
	"components": {
		"schemas": {
			"ErrorEnvelope": {
				"type": "object",
				"description": "Every failure, whatever its status.",
				"properties": {
					"error": {
						"type": "object",
						"properties": {
							"code": {
								"type": "string",
								"description": "A closed set this API owns"
							},
							"message": {
								"type": "string"
							}
						},
						"required": [
							"code",
							"message"
						]
					}
				},
				"required": [
					"error"
				]
			},
			"ListEnvelope": {
				"type": "object",
				"description": "Every list answer. `nextCursor` is present only when a page follows.",
				"properties": {
					"data": {
						"type": "array",
						"items": {
							"type": "object"
						}
					},
					"count": {
						"type": "integer"
					},
					"nextCursor": {
						"type": "string"
					}
				},
				"required": [
					"data",
					"count"
				]
			},
			"CreatePortalRoleBody": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string",
						"description": "The label people see"
					},
					"description": {
						"type": "string",
						"description": "What the role is for"
					},
					"pageGrants": {
						"type": "string",
						"description": "The pages it opens and the verbs on each"
					},
					"fieldGrants": {
						"type": "string",
						"description": "How each restrictable field reads to a holder"
					}
				},
				"required": [
					"name",
					"description",
					"pageGrants"
				],
				"additionalProperties": false
			},
			"UpdatePortalRoleBody": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string",
						"description": "The label people see"
					},
					"description": {
						"type": "string",
						"description": "What the role is for"
					},
					"pageGrants": {
						"type": "string",
						"description": "The pages it opens and the verbs on each"
					},
					"fieldGrants": {
						"type": "string",
						"description": "How each restrictable field reads to a holder"
					}
				},
				"additionalProperties": false
			},
			"UpdatePortalOrganisationBody": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string"
					},
					"email": {
						"type": "string"
					},
					"phone": {
						"type": "string"
					},
					"website": {
						"type": "string"
					},
					"description": {
						"type": "string"
					},
					"latitude": {
						"type": "number"
					},
					"longitude": {
						"type": "number"
					},
					"city": {
						"type": "string"
					},
					"country": {
						"type": "string"
					},
					"postalCode": {
						"type": "string"
					},
					"pages": {
						"type": "string",
						"description": "The page ids its people may reach. Only a super may change it"
					}
				},
				"additionalProperties": false
			},
			"UpdatePortalSettingsBody": {
				"type": "object",
				"properties": {
					"smsEnabled": {
						"type": "boolean"
					},
					"termsUrl": {
						"type": "string"
					},
					"schedules": {
						"type": "object",
						"properties": {
							"maturity-detector": {
								"oneOf": [
									{
										"type": "object",
										"properties": {
											"interval": {
												"type": "string",
												"enum": [
													"hourly"
												]
											}
										},
										"required": [
											"interval"
										],
										"additionalProperties": false
									},
									{
										"type": "object",
										"properties": {
											"interval": {
												"type": "string",
												"enum": [
													"daily"
												]
											},
											"hourUtc": {
												"type": "integer"
											}
										},
										"required": [
											"interval",
											"hourUtc"
										],
										"additionalProperties": false
									}
								]
							},
							"interest-accrual": {
								"oneOf": [
									{
										"type": "object",
										"properties": {
											"interval": {
												"type": "string",
												"enum": [
													"hourly"
												]
											}
										},
										"required": [
											"interval"
										],
										"additionalProperties": false
									},
									{
										"type": "object",
										"properties": {
											"interval": {
												"type": "string",
												"enum": [
													"daily"
												]
											},
											"hourUtc": {
												"type": "integer"
											}
										},
										"required": [
											"interval",
											"hourUtc"
										],
										"additionalProperties": false
									}
								]
							},
							"liability-starter": {
								"oneOf": [
									{
										"type": "object",
										"properties": {
											"interval": {
												"type": "string",
												"enum": [
													"hourly"
												]
											}
										},
										"required": [
											"interval"
										],
										"additionalProperties": false
									},
									{
										"type": "object",
										"properties": {
											"interval": {
												"type": "string",
												"enum": [
													"daily"
												]
											},
											"hourUtc": {
												"type": "integer"
											}
										},
										"required": [
											"interval",
											"hourUtc"
										],
										"additionalProperties": false
									}
								]
							}
						},
						"additionalProperties": false
					}
				},
				"additionalProperties": false
			},
			"CreatePortalTokenBody": {
				"type": "object",
				"properties": {
					"username": {
						"type": "string",
						"description": "The email address or phone number you sign in with"
					},
					"password": {
						"type": "string",
						"description": "Your password"
					}
				},
				"required": [
					"username",
					"password"
				],
				"additionalProperties": false
			},
			"CreateProductVersionBody": {
				"type": "object",
				"properties": {
					"currency": {
						"type": "string",
						"enum": [
							"XAF"
						],
						"description": "The market this version prices"
					},
					"accrualPeriod": {
						"type": "string",
						"enum": [
							"WEEK",
							"MONTH"
						],
						"description": "Whether interest builds up weekly or monthly"
					},
					"accrualPeriods": {
						"type": "integer",
						"description": "How many of those periods a position earns over"
					},
					"interestRate": {
						"type": "string",
						"pattern": "^\\d+(\\.\\d+)?$",
						"description": "Annual interest rate as a decimal string"
					},
					"discountRate": {
						"type": "string",
						"pattern": "^\\d+(\\.\\d+)?$",
						"description": "The share of earned interest given up by leaving early"
					},
					"minAmount": {
						"type": "string",
						"pattern": "^\\d+$",
						"description": "The least a customer can pay into this product"
					},
					"maxBalance": {
						"type": "string",
						"pattern": "^\\d+$",
						"description": "The most one position in this product may hold"
					},
					"earlyExitHoldSeconds": {
						"type": "integer",
						"description": "How long an early-exit payout is held in seconds"
					},
					"effectiveFrom": {
						"type": "string",
						"description": "When this version takes effect. A future date schedules it"
					},
					"status": {
						"type": "string",
						"enum": [
							"ACTIVE",
							"RETIRED"
						],
						"description": "Whether this version is offered for new deposits"
					}
				},
				"required": [
					"currency",
					"accrualPeriod",
					"accrualPeriods",
					"interestRate",
					"discountRate",
					"minAmount",
					"maxBalance",
					"earlyExitHoldSeconds",
					"effectiveFrom",
					"status"
				],
				"additionalProperties": false
			},
			"FundPoolBody": {
				"type": "object",
				"properties": {
					"reference": {
						"type": "string",
						"pattern": "^[A-Za-z0-9_-]{1,64}$",
						"description": "The caller's own key for this top-up"
					},
					"currency": {
						"type": "string",
						"enum": [
							"XAF"
						],
						"description": "The market whose pool is being topped up"
					},
					"amount": {
						"type": "string",
						"pattern": "^\\d+$",
						"description": "How much the business is putting in"
					},
					"occurredAt": {
						"type": "string",
						"description": "When the money moved. Absent means now"
					}
				},
				"required": [
					"reference",
					"currency",
					"amount"
				],
				"additionalProperties": false
			},
			"AnswerPortalChallengeBody": {
				"type": "object",
				"properties": {
					"username": {
						"type": "string",
						"description": "The email address or phone number you sign in with"
					},
					"password": {
						"type": "string",
						"description": "The password the challenge was raised on"
					},
					"newPassword": {
						"type": "string",
						"description": "Required when the challenge is a new password"
					},
					"code": {
						"type": "string",
						"description": "Required when the challenge is an MFA code"
					}
				},
				"required": [
					"username",
					"password"
				],
				"additionalProperties": false
			},
			"StartPortalPasswordResetBody": {
				"type": "object",
				"properties": {
					"username": {
						"type": "string",
						"description": "The email address or phone number you sign in with"
					}
				},
				"required": [
					"username"
				],
				"additionalProperties": false
			},
			"ConfirmPortalPasswordResetBody": {
				"type": "object",
				"properties": {
					"username": {
						"type": "string",
						"description": "The email address or phone number you sign in with"
					},
					"code": {
						"type": "string",
						"description": "The code sent to your alias"
					},
					"password": {
						"type": "string",
						"description": "The password to set"
					}
				},
				"required": [
					"username",
					"code",
					"password"
				],
				"additionalProperties": false
			},
			"ChangePortalPasswordBody": {
				"type": "object",
				"properties": {
					"previousPassword": {
						"type": "string",
						"description": "Your current password"
					},
					"proposedPassword": {
						"type": "string",
						"description": "The password to set"
					}
				},
				"required": [
					"previousPassword",
					"proposedPassword"
				],
				"additionalProperties": false
			},
			"EnablePortalMfaBody": {
				"type": "object",
				"properties": {
					"code": {
						"type": "string",
						"description": "The first code from your authenticator app"
					}
				},
				"additionalProperties": false
			},
			"CreatePortalUserBody": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string",
						"description": "What the portal greets them by"
					},
					"email": {
						"type": "string",
						"description": "An email sign-in alias"
					},
					"phoneNumber": {
						"type": "string",
						"pattern": "^\\+[1-9]\\d{6,14}$",
						"description": "A phone-number sign-in alias"
					},
					"organisations": {
						"type": "string",
						"description": "Whose data they are scoped to"
					},
					"roles": {
						"type": "string",
						"description": "What authority they are given"
					}
				},
				"required": [
					"organisations",
					"roles"
				],
				"additionalProperties": false
			},
			"UpdatePortalUserBody": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string",
						"description": "What the portal greets them by"
					},
					"email": {
						"type": "string",
						"description": "An email alias to add. Only for a user who holds none"
					},
					"phoneNumber": {
						"type": "string",
						"pattern": "^\\+[1-9]\\d{6,14}$",
						"description": "A phone alias to add. Only for a user who holds none"
					},
					"organisations": {
						"type": "string",
						"description": "The complete set they should end in, not a delta"
					},
					"roles": {
						"type": "string",
						"description": "The complete set they should end in, not a delta"
					},
					"enabled": {
						"type": "boolean",
						"description": "False disables, true re-enables"
					}
				},
				"additionalProperties": false
			}
		},
		"securitySchemes": {
			"bearerAuth": {
				"type": "http",
				"scheme": "bearer",
				"bearerFormat": "JWT",
				"description": "The `accessToken` from `POST /auth/token`. It lasts an hour."
			}
		}
	}
}
