openapi: 3.1.0 info: title: LokaDB Cloud API version: "1" summary: Move a customer's data to the customer's own Postgres from your product. description: | The API a SaaS calls from its backend with its API key (`lss_…`, made in the panel under Account → API keys). It asks for a move of one organization to the organization's own Postgres, hands the organization's administrator an install command and a customer portal link, follows the move, and moves it back. Never call it from a browser: the key is the account's. Keys act as a member of the account; a read-only key may only read. The routes of the panel, the runner, the installer, the agent and our operators are not part of this API. **Rate limits.** Each key may make 600 requests a minute, and each address 1,200 (counted before the key is checked), with bursts of 10 seconds' worth. Past that a call answers 429 (`code: rate_limited`) with `Retry-After`, the seconds to wait. A move's stream (`/v1/moves/{id}/stream`) is one request however long it stays open: follow it rather than poll, or poll a move at most once every few seconds. contact: email: hello@lokadb.com servers: - url: https://api.lokadb.com security: - apiKey: [] tags: - name: moves description: A move of one organization to its own database, and back. - name: installations description: Your databases as your runner reports them, and what is moved out of them. - name: runners description: The runner, the open `lokadb runner` next to your proxy, that does the database work. - name: jobs description: The runner's work for an installation (a check, enable, a move's steps). - name: account description: The plan and its use, the account's webhook receiver. - name: status description: The runner's and the agents' status, series and alerts. - name: service description: About the service itself. - name: billing x-internal: true description: | Billing by Paddle. Not part of the API a product calls: Paddle posts the webhook, and the panel's Plan page calls the rest. In the spec so that every route is described; left out of the published reference. paths: /v1/moves: post: tags: [moves] operationId: createMove summary: Ask for a move of an organization description: | Makes a move request of one organization and its install command. Give the organization's administrator the command (`install.command`) and the customer portal link (`portal_url`): both are shown only in this answer. The runner checks that the organization exists first, so the call takes up to a few seconds. **Retrying after a timeout.** If the call timed out or the connection broke, ask again: a move that was made answers `409` with `code: live_move` and its `id`. Its install command and customer portal link were in the lost answer only: make new ones with `POST /v1/moves/{id}/token` and `POST /v1/moves/{id}/portal`. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/CreateMove'} examples: basic: {value: {org: 7, external_ref: "acme", callback_url: "https://saas.example.com/hooks/lokadb"}} responses: '201': description: The move, with its install command and customer portal link. content: application/json: schema: {$ref: '#/components/schemas/Move'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '402': {$ref: '#/components/responses/PlanLimit'} '403': {$ref: '#/components/responses/Forbidden'} '404': description: 'The `installation` you named is not one of your account''s.' content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} '409': description: '`code: live_move`: the organization has a live move already (`id` names it; see "Retrying after a timeout"). `code: no_installation`: no database is connected yet (set up the runner first).' content: application/json: schema: {$ref: '#/components/schemas/Error'} '428': {$ref: '#/components/responses/ConsentRequired'} '503': description: '`code: runner_unavailable`: your runner did not answer (it is down or not connected; see `/v1/accounts/{a}/runners`). Nothing was made: ask again later.' content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} get: tags: [moves] operationId: listMoves summary: List moves description: The account's moves, newest first (up to 200). parameters: - {name: installation, in: query, schema: {type: string}, description: 'Only this installation''s.'} - {name: org, in: query, schema: {type: integer, minimum: 1}, description: 'Only this organization''s.'} - {name: state, in: query, schema: {$ref: '#/components/schemas/MoveState'}} - {name: q, in: query, schema: {type: string}, description: 'Matches the organization''s number, its tenant or the external reference.'} responses: '200': description: The moves. content: application/json: schema: type: object required: [moves] properties: moves: {type: array, items: {$ref: '#/components/schemas/Move'}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} /v1/moves/{id}: get: tags: [moves] operationId: getMove summary: A move's status parameters: [{$ref: '#/components/parameters/MoveID'}] responses: '200': description: The move. content: application/json: schema: {$ref: '#/components/schemas/Move'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/moves/{id}/events: get: tags: [moves] operationId: moveEvents summary: A move's events description: Its progress lines and state changes after `after` (a sequence number), up to 500. parameters: - {$ref: '#/components/parameters/MoveID'} - {name: after, in: query, schema: {type: integer, default: 0}} responses: '200': description: The events. content: application/json: schema: type: object required: [state, events] properties: state: {$ref: '#/components/schemas/MoveState'} events: {type: array, items: {$ref: '#/components/schemas/MoveEvent'}} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/moves/{id}/stream: get: tags: [moves] operationId: moveStream summary: A move's events as a stream description: | Server-sent events: `event: status` (data: `{state, phase, progress, error, warning}`) whenever the move changes, and one event per line of progress (`event:` its kind, `id:` its sequence number, data: a MoveEvent). Reconnect with `Last-Event-ID` to go on where it stopped. The stream ends when the move is in a final state. parameters: - {$ref: '#/components/parameters/MoveID'} - {name: after, in: query, schema: {type: integer}, description: 'Start after this sequence number.'} - {name: Last-Event-ID, in: header, schema: {type: string}} responses: '200': description: The stream. content: text/event-stream: schema: {type: string} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/moves/{id}/report: get: tags: [moves] operationId: moveReport summary: The move's check report description: The full report of `lokadb check -json` (schema 1) on the organization's database. 404 before the check ran. parameters: [{$ref: '#/components/parameters/MoveID'}] responses: '200': description: The report. content: application/json: schema: {$ref: '#/components/schemas/CheckReport'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/moves/{id}/{action}: post: tags: [moves] operationId: moveAction summary: Act on a move description: | `actions` in the move says which are allowed now; any other answers 409. - `token`: a new install command (the old token stops working); - `approve`: let a checked move start (when `require_approval`); - `recheck`: run the check again after a fix; - `retry`: start a failed step again; - `cancel`: stop before the move switches (the agent is revoked); - `move-back`: bring the organization back to your database; - `portal`: a new customer portal link (the old one stops working). parameters: - {$ref: '#/components/parameters/MoveID'} - name: action in: path required: true schema: {type: string, enum: [token, approve, recheck, retry, cancel, move-back, portal]} responses: '200': description: The move after the action (`install` after `token`, `portal_url` after `portal`). content: application/json: schema: {$ref: '#/components/schemas/Move'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '409': description: Not allowed in the move's current state (`state` and a hint with the allowed actions). content: application/json: schema: {$ref: '#/components/schemas/Error'} /v1/accounts/{a}/installations: get: tags: [installations] operationId: listInstallations summary: List installations description: Your databases, one per SaaS database a runner serves. parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '200': description: The installations. content: application/json: schema: type: object required: [installations] properties: installations: {type: array, items: {$ref: '#/components/schemas/Installation'}} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}: get: tags: [installations] operationId: getInstallation summary: An installation and its runner parameters: [{$ref: '#/components/parameters/InstallationID'}] responses: '200': description: The installation. content: application/json: schema: type: object required: [installation] properties: installation: {$ref: '#/components/schemas/Installation'} runner: {$ref: '#/components/schemas/Runner'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/orgs: get: tags: [installations] operationId: listOrgs summary: Organizations at customers and with a live move parameters: - {$ref: '#/components/parameters/InstallationID'} - {name: location, in: query, schema: {type: string, enum: [cloud, customer, moving, returning, billable]}} responses: '200': description: The organizations and their counts. content: application/json: schema: type: object required: [items, counts] properties: items: {type: array, items: {$ref: '#/components/schemas/Org'}} counts: type: object properties: cloud: {type: integer} customer: {type: integer} moving: {type: integer} returning: {type: integer} billable: {type: integer} next_cursor: {type: ['string', 'null']} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/orgs/{org}: get: tags: [installations] operationId: getOrg summary: One organization parameters: [{$ref: '#/components/parameters/InstallationID'}, {$ref: '#/components/parameters/Org'}] responses: '200': description: The organization, its live move and its moves. content: application/json: schema: {$ref: '#/components/schemas/OrgCard'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [installations] operationId: labelOrg summary: Name an organization description: Your name and reference for the organization (shown in the panel). parameters: [{$ref: '#/components/parameters/InstallationID'}, {$ref: '#/components/parameters/Org'}] requestBody: required: true content: application/json: schema: type: object properties: name: {type: string, maxLength: 200} external_ref: {type: string, maxLength: 200} responses: '200': description: The organization. content: application/json: schema: {$ref: '#/components/schemas/OrgCard'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/extensions: get: tags: [installations] operationId: installationExtensions summary: The extensions your tables need, per organization at a customer description: Look here before you deploy a migration that adds an extension. parameters: [{$ref: '#/components/parameters/InstallationID'}] responses: '200': description: Per extension, the organizations that have it and those that do not. content: application/json: schema: type: array items: type: object required: [name, with, without] properties: name: {type: string} schema: {type: string} cloud_version: {type: string} trusted: {type: boolean} with: {type: array, items: {type: integer}} without: type: array items: type: object properties: org: {type: integer} status: {type: string, enum: [present, created, needs_admin, unavailable, failed, unknown]} version: {type: string} error: {type: string} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/report: get: tags: [installations] operationId: installationReport summary: The latest check of your database parameters: [{$ref: '#/components/parameters/InstallationID'}] responses: '200': description: The report of `lokadb check -json`. content: application/json: schema: {$ref: '#/components/schemas/CheckReport'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/jobs: get: tags: [jobs] operationId: listJobs summary: The installation's latest jobs parameters: [{$ref: '#/components/parameters/InstallationID'}] responses: '200': description: Up to 100 jobs, newest first. content: application/json: schema: type: object required: [jobs] properties: jobs: {type: array, items: {$ref: '#/components/schemas/Job'}} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/check: post: tags: [jobs] operationId: checkInstallation summary: Check your database description: Runs `lokadb check` on the SaaS database alone. Follow the job; its result holds the report. parameters: [{$ref: '#/components/parameters/InstallationID'}] responses: '202': description: The job, queued for the runner. content: application/json: schema: {$ref: '#/components/schemas/JobAnswer'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/enable: post: tags: [jobs] operationId: enableInstallation summary: Enable LokaDB in your database description: The account's owner only, from the panel (an API key gets 403). Type the database's name to confirm. parameters: [{$ref: '#/components/parameters/InstallationID'}] requestBody: required: true content: application/json: schema: type: object required: [confirm_db] properties: confirm_db: {type: string} responses: '202': description: The job. content: application/json: schema: {$ref: '#/components/schemas/JobAnswer'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /v1/jobs/{id}: get: tags: [jobs] operationId: getJob summary: A job parameters: [{$ref: '#/components/parameters/JobID'}] responses: '200': description: The job; a check's `result.report` is its report. content: application/json: schema: {$ref: '#/components/schemas/JobAnswer'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/jobs/{id}/events: get: tags: [jobs] operationId: jobEvents summary: A job's events parameters: - {$ref: '#/components/parameters/JobID'} - {name: after, in: query, schema: {type: integer, default: 0}} responses: '200': description: The events. content: application/json: schema: type: object properties: state: {type: string} events: {type: array, items: {$ref: '#/components/schemas/JobEvent'}} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/jobs/{id}/stream: get: tags: [jobs] operationId: jobStream summary: A job's events as a stream description: 'Server-sent events, `event: state` then one per line of progress; ends with the job.' parameters: [{$ref: '#/components/parameters/JobID'}] responses: '200': description: The stream. content: text/event-stream: schema: {type: string} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/accounts/{a}/runners/registrations: post: tags: [runners] operationId: registerRunner summary: A one-time command to start a runner description: The token works once, for 30 minutes. The runner keeps its own secret afterwards. parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '201': description: The command. content: application/json: schema: type: object required: [token, command, expires_at] properties: token: {type: string, description: '`lrr_…`, shown once.'} command: {type: string, description: '`docker run …` of the current release''s image, mounting `/etc/lokadb` (where `runner.json` goes).'} image: {type: string, description: 'The runner''s image with the current release''s tag, e.g. `ghcr.io/lokadb/lokadb:v0.9.0`.'} expires_at: {type: string, format: date-time} service: {type: string, description: 'This API''s address: `service` in `runner.json`.'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/accounts/{a}/runners: get: tags: [runners] operationId: listRunners summary: List runners parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '200': description: The runners. content: application/json: schema: type: object required: [runners] properties: runners: {type: array, items: {$ref: '#/components/schemas/Runner'}} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/runners/{r}: delete: tags: [runners] operationId: removeRunner summary: Remove a runner description: The account's owner only, from the panel (an API key gets 403). Its secret stops working. parameters: - {name: r, in: path, required: true, schema: {type: string}} responses: '204': {description: 'Removed.'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /v1/accounts/{a}/plan: get: tags: [account] operationId: getPlan summary: The plan and its use parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '200': description: The plan. content: application/json: schema: {$ref: '#/components/schemas/Plan'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/accounts/{a}/usage: get: tags: [account] operationId: getUsage summary: Organizations in use per day parameters: - {$ref: '#/components/parameters/AccountID'} - {name: from, in: query, schema: {type: string, format: date}, description: 'Default 29 days ago.'} - {name: to, in: query, schema: {type: string, format: date}, description: 'Default today; at most 400 days after `from`.'} responses: '200': description: One row per day. content: application/json: schema: type: object properties: plan: {type: string} org_limit: {type: integer} days: type: array items: type: object properties: day: {type: string, format: date} orgs_billable: {type: integer} over_limit: {type: integer} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/accounts/{a}/webhook: get: tags: [account] operationId: getWebhook summary: The account's webhook receiver parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '200': description: The receiver (`url` null when none). The secret is never shown again. content: application/json: schema: {$ref: '#/components/schemas/WebhookEndpoint'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} put: tags: [account] operationId: setWebhook summary: Set the account's webhook receiver description: | Moves without their own `callback_url` are posted here, signed with this receiver's secret. The secret (`whs_…`) is in the answer of the first PUT and of a PUT with `rotate: true`, and nowhere else. parameters: [{$ref: '#/components/parameters/AccountID'}] requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: {type: string, format: uri} events: {type: array, items: {type: string, enum: [move.state]}, description: 'Only `move.state` is sent; empty means the same.'} rotate: {type: boolean} responses: '200': description: The receiver. content: application/json: schema: {$ref: '#/components/schemas/WebhookEndpoint'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} delete: tags: [account] operationId: deleteWebhook summary: Remove the account's webhook receiver parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '204': {description: 'Removed.'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/status: get: tags: [status] operationId: getStatus summary: The runner's last status snapshot parameters: [{$ref: '#/components/parameters/InstallationID'}] responses: '200': description: '`runner` is the snapshot as the runner sent it (null before the first), `at` when it came.' content: application/json: schema: type: object properties: at: {type: string, format: date-time} runner: {type: ['object', 'null']} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/agents: get: tags: [status] operationId: getAgents summary: The agents of the organizations at customers parameters: [{$ref: '#/components/parameters/InstallationID'}] responses: '200': description: One row per agent. content: application/json: schema: type: object properties: agents: type: array items: type: object properties: org: {type: string} state: {type: string} last_state: {type: string} connected: {type: boolean} version: {type: string} cert_ends: {type: string, format: date-time} renew_error: {type: string} db_ok: {type: boolean} rtt_ms: {type: number} remote: {type: string} last_seen_at: {type: string, format: date-time} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/series: get: tags: [status] operationId: getSeries summary: Time series of the organizations at customers parameters: - {$ref: '#/components/parameters/InstallationID'} - {name: metric, in: query, required: true, schema: {type: string}, description: 'Up to 20, comma separated: agent_connected, probe_ok, rtt_ms, db_bytes, streams, relay_sessions, queries, errors, fdw, direct, p50_ms, p95_ms.'} - {name: org, in: query, schema: {type: string}} - {name: from, in: query, required: true, schema: {type: string, format: date-time}} - {name: to, in: query, required: true, schema: {type: string, format: date-time}} - {name: step, in: query, schema: {type: string, enum: [1m, 1h]}} responses: '200': description: The series; points are [time, value]. content: application/json: schema: type: object properties: series: type: array items: type: object properties: org: {type: string} metric: {type: string} points: {type: array, items: {type: array}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/installations/{i}/series/summary: get: tags: [status] operationId: getSummary summary: Availability and errors over a period parameters: - {$ref: '#/components/parameters/InstallationID'} - {name: from, in: query, required: true, schema: {type: string, format: date-time}} - {name: to, in: query, required: true, schema: {type: string, format: date-time}} responses: '200': description: The summary. content: application/json: schema: type: object properties: orgs_customer: {type: integer} availability_pct: {type: ['number', 'null']} error_pct: {type: ['number', 'null']} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} /v1/accounts/{a}/alerts: get: tags: [status] operationId: getAlerts summary: Alert rules and recent alerts parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '200': description: The rules and the alerts. content: application/json: schema: type: object properties: rules: {type: array, items: {$ref: '#/components/schemas/AlertRule'}} alerts: type: array items: type: object properties: installation: {type: string} rule: {type: string} target: {type: string} state: {type: string, enum: [firing, resolved]} since: {type: string, format: date-time} resolved_at: {type: string, format: date-time} detail: {type: string} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '404': {$ref: '#/components/responses/NotFound'} put: tags: [status] operationId: setAlerts summary: Set the alert rules parameters: [{$ref: '#/components/parameters/AccountID'}] requestBody: required: true content: application/json: schema: type: object required: [rules] properties: rules: {type: array, items: {$ref: '#/components/schemas/AlertRule'}} responses: '200': description: The rules as saved. content: application/json: schema: type: object '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/TooMany'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /v1/versions: get: tags: [service] operationId: getVersions summary: The current releases description: Compare with your runner's and your customers' agents' versions. security: [] responses: '200': description: The versions. content: application/json: schema: type: object properties: runner: {type: string, description: 'The runner''s release (its image tag).'} runner_image: {type: string, description: 'The runner''s image without a tag: `ghcr.io/lokadb/lokadb`.'} agent: {type: string} installer_image: {type: string} /v1/billing/paddle: post: tags: [billing] x-internal: true operationId: paddleWebhook summary: Paddle's webhook (Paddle only) description: | Paddle posts its subscription and transaction events here, signed with the notification secret (`Paddle-Signature`). An event already applied answers 200 again. Nothing else should call it. security: [] responses: '200': {description: Taken (or already applied).} '400': {$ref: '#/components/responses/BadRequest'} '401': {description: 'A bad signature.', content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}} /v1/accounts/{a}/billing: get: tags: [billing] x-internal: true operationId: billingState summary: The account's billing (the panel's Plan page) parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '200': description: The subscription, the plan and Paddle.js' client-side settings. content: {application/json: {schema: {type: object}}} '401': {$ref: '#/components/responses/Unauthorized'} '404': {$ref: '#/components/responses/NotFound'} /v1/accounts/{a}/billing/portal: post: tags: [billing] x-internal: true operationId: billingPortal summary: A link to Paddle's billing portal (the owner, from the panel) parameters: [{$ref: '#/components/parameters/AccountID'}] responses: '200': description: The link. content: {application/json: {schema: {type: object, properties: {url: {type: string}}}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '409': {description: No subscription yet., content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}} '502': {description: Paddle did not answer., content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}} /v1/accounts/{a}/billing/plan: post: tags: [billing] x-internal: true operationId: billingPlan summary: Change the plan of a live subscription (the owner, from the panel) parameters: [{$ref: '#/components/parameters/AccountID'}] requestBody: required: true content: application/json: schema: {type: object, required: [plan], properties: {plan: {type: string, enum: [starter, growth]}}} responses: '200': description: Already on that plan. content: {application/json: {schema: {type: object, properties: {result: {type: string}}}}} '202': description: Asked of Paddle; the plan changes when Paddle confirms it. content: {application/json: {schema: {type: object, properties: {result: {type: string}}}}} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} '409': {description: 'No live subscription, an enterprise plan, or a subscription that is not active.', content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}} webhooks: moveState: post: summary: A move changed state description: | Posted to the move's `callback_url`, else to the account's receiver, at least once per state change (deduplicate by `X-Lokadb-Delivery`). Retried until your receiver answers 2xx: after 5 s, then doubling up to an hour between tries, for 24 hours. Set the account's receiver (`PUT /v1/accounts/{a}/webhook`) first even when you give each move its own `callback_url`: its secret signs every delivery of the account. **Signature.** `X-Lokadb-Signature: sha256=` is HMAC-SHA256 with your webhook secret over `.` (the timestamp, a dot, the raw body). Compute it over the raw bytes before parsing, compare in constant time, and refuse a timestamp older than 5 minutes. parameters: - {name: X-Lokadb-Timestamp, in: header, required: true, schema: {type: string}, description: 'Unix seconds.'} - {name: X-Lokadb-Signature, in: header, required: true, schema: {type: string}, description: '`sha256=` and the hex HMAC.'} - {name: X-Lokadb-Delivery, in: header, schema: {type: string}, description: 'The delivery''s id (the same on its retries).'} requestBody: content: application/json: schema: {$ref: '#/components/schemas/WebhookBody'} responses: '200': {description: 'Taken. Any 2xx counts.'} components: securitySchemes: apiKey: type: http scheme: bearer description: '`Authorization: Bearer lss_…`, made in the panel (Account → API keys). Server-side only.' parameters: MoveID: {name: id, in: path, required: true, schema: {type: string, example: mv_8f2c1a9e04b7d3c5e6f1}} AccountID: {name: a, in: path, required: true, schema: {type: string}, description: 'Your account''s id (the panel''s URL, or `account` in a move).'} InstallationID: {name: i, in: path, required: true, schema: {type: string, example: in_3784bf530c9a4b4c}} JobID: {name: id, in: path, required: true, schema: {type: string, example: jb_1c2d3e4f5a6b7c8d9e0f}} Org: {name: org, in: path, required: true, schema: {type: integer, minimum: 1}} responses: BadRequest: description: 'A field is missing or wrong; on `POST /v1/moves` also an organization your runner does not find in your database.' content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} Unauthorized: description: No valid API key (or session). `login_url` is for the panel. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} PlanLimit: description: '`code: plan_limit` (and `error` starts with `plan_limit:`): the plan''s organizations are all in use (a live move or at a customer). Upgrade, or move one back.' content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} Forbidden: description: 'The account is pending (`account_pending`, activated by us after "request access"), suspended, or the route is the owner''s, or the key is read-only.' content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} NotFound: description: No such object in your account (another account's ids answer the same). content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} ConsentRequired: description: '`consent_required`: a signed-in person has to accept new versions of the Terms first (panel sessions only).' content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} TooMany: description: '`rate_limited`: over the key''s or the address''s rate (see Rate limits); wait `Retry-After` seconds.' headers: Retry-After: {schema: {type: integer}} content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} RunnerSilent: description: Your runner did not answer (it is down or not connected); see `/v1/accounts/{a}/runners`. content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}} schemas: Error: type: object required: [error] properties: error: {type: string, description: 'What went wrong, in words.'} code: {type: string, description: 'A stable code where there is one: unauthenticated, account_pending, consent_required, read_only_key, forbidden, rate_limited; on POST /v1/moves also plan_limit, live_move, no_installation, runner_unavailable.'} hint: {type: string, description: 'What to do.'} field: {type: string, description: 'The field a 400 is about.'} id: {type: string, description: 'On 409 of POST /v1/moves: the live move of the organization.'} state: {type: string, description: 'On 409 of an action: the move''s state.'} login_url: {type: string} CreateMove: type: object properties: installation: {type: string, description: 'The installation; may be left out when the account has one.'} org: {type: integer, minimum: 1, description: 'The organization''s number. Give `org` or `tenant`.'} tenant: {type: string, description: 'The organization''s uuid, when your tenants are uuids.'} require_approval: {type: boolean, description: 'Wait for `approve` after the check. Default `false`: the move starts as soon as the check passes (verdict `ready` or `review`). The panel''s form ticks it by default.'} customer_can_move_back: {type: boolean, description: 'Let the organization''s administrator move back from the customer portal.'} callback_url: {type: string, format: uri, description: 'Where this move''s state changes are posted (else the account''s receiver).'} external_ref: {type: string, description: 'Your own reference (a customer id), echoed in the move and its webhooks.'} customer_email: type: string format: email maxLength: 254 description: | The organization's administrator's address. It gets the alerts about their side only (`agent_gone`, `customer_db_unreachable`, `extension_needed`), on plans with alerts; your account's own addresses get every alert. additionalProperties: false MoveState: type: string enum: [token_issued, agent_connected, checking, checked, blocked, moving, moved, moving_back, moved_back, failed, cancelled] description: | `token_issued` → (the administrator runs the command) `agent_connected` → `checking` → `checked` (or `blocked` by a finding) → `moving` → `moved`; `moving_back` → `moved_back`. `failed` has a `phase` (check, move, move_back). `moved_back` and `cancelled` are final. Move: type: object required: [id, org, state, actions, created_at] properties: id: {type: string} account: {type: string} installation: {type: string} org: {type: integer} tenant: {type: string} external_ref: {type: string} customer_email: {type: string, description: 'As given when the move was made.'} state: {$ref: '#/components/schemas/MoveState'} phase: {type: string, enum: [check, move, move_back]} error: {type: string} warning: {type: string} progress: {type: string, description: 'The latest line of progress.'} actions: {type: array, items: {type: string}, description: 'What POST /v1/moves/{id}/{action} allows now.'} require_approval: {type: boolean} approved: {type: boolean} customer_can_move_back: {type: boolean} callback_url: {type: string} install: type: object description: Only in the answer that made the token. properties: command: {type: string, description: '`docker run -it …`: the organization''s administrator runs it in their network.'} command_noninteractive: {type: string} token: {type: string, description: '`lsi_…`, works for 30 minutes.'} expires_at: {type: string, format: date-time} install_expires_at: {type: string, format: date-time, description: 'When the current install command stops working; in every answer. `install.expires_at` is the same time, in the answer that made the command.'} portal_url: {type: string, description: 'The customer portal: a page LokaDB hosts for the organization''s administrator, without an account. A secret (whoever holds it gets install commands and may recheck or move back); valid until a new one is made (`portal`). Only in the answer that made the link.'} agent_update_command: {type: string} database: {type: string, description: 'The organization''s database (after the agent connected).'} db_addr: {type: string} pg_version: {type: string} installer_version: {type: string} hostname: {type: string} relay_port: {type: integer} agent_connected_at: {type: string, format: date-time} agent_seen_at: {type: string, format: date-time} agent_status: {type: object} check_verdict: type: string enum: [ready, review, unsupported, blocked, invalid_spec] description: | The check's verdict. `ready` and `review` pass (the state becomes `checked`; `review` means changes or warnings in `findings`, and the move still starts on its own unless `require_approval`). `unsupported`, `blocked` and `invalid_spec` stop it (`blocked` state) until the findings are fixed and a `recheck` passes. check_exit: {type: integer} findings: {type: array, items: {$ref: '#/components/schemas/Finding'}} extensions: {type: array, items: {$ref: '#/components/schemas/ExtensionStatus'}} profile: {type: string, description: 'The organization''s routing once moved (fdw, fdw+direct, direct).'} created_by: {type: object, properties: {kind: {type: string}, id: {type: string}}} run_started_at: {type: string, format: date-time} created_at: {type: string, format: date-time} updated_at: {type: string, format: date-time} moved_at: {type: string, format: date-time} moved_back_at: {type: string, format: date-time} Finding: type: object properties: severity: {type: string, enum: [spec_error, blocker, unsupported, change, warning, info]} code: {type: string} object: {type: string} message: {type: string} who: {type: string, enum: [saas, customer], description: 'Who has to act on it.'} ExtensionStatus: type: object properties: name: {type: string} status: {type: string, enum: [present, created, needs_admin, unavailable, failed]} version: {type: string} cloud_version: {type: string} error: {type: string} MoveEvent: type: object properties: seq: {type: integer} at: {type: string, format: date-time} kind: {type: string, enum: [state, progress, warning, error, note]} state: {type: string} msg: {type: string} attrs: {type: object} CheckReport: type: object description: 'The report of `lokadb check -json`, schema 1: `verdict`, `findings`, `tables` and more.' properties: schema_version: {type: integer} verdict: {type: string} findings: {type: array, items: {type: object}} Installation: type: object properties: id: {type: string} account: {type: string} installation_key: {type: string} database: {type: string} runner: {type: string} enabled: {type: boolean} inventory: {type: object} inventory_at: {type: string, format: date-time} status: {type: object} status_at: {type: string, format: date-time} created_at: {type: string, format: date-time} Runner: type: object properties: id: {type: string} account: {type: string} name: {type: string} hostname: {type: string} version: {type: string} public_key: {type: string} fingerprint: {type: string, description: 'The key the installers show to the organizations'' administrators.'} capabilities: {type: array, items: {type: string}} installation: {type: string} status: {type: string, enum: [active, standby, removed]} registered_at: {type: string, format: date-time} last_seen_at: {type: string, format: date-time} online: {type: boolean} Org: type: object properties: org: {type: integer} location: {type: string, enum: [cloud, customer, moving, returning]} profile: {type: string} move: {type: string} state: {type: string} tenant: {type: string} external_ref: {type: string} name: {type: string} OrgCard: type: object properties: org: {type: integer} location: {type: string} profile: {type: string} name: {type: string} external_ref: {type: string} move: {$ref: '#/components/schemas/Move'} moves: {type: array, items: {type: object}} extensions: {type: array, items: {$ref: '#/components/schemas/ExtensionStatus'}} findings: {type: array, items: {$ref: '#/components/schemas/Finding'}} agent: {type: object} Job: type: object properties: id: {type: string} account: {type: string} installation: {type: string} runner: {type: string} move_id: {type: string} kind: {type: string, enum: [inventory, org.lookup, check, enable, agent.prepare, credentials.store, move, move_back, org.release, extensions.sync]} v: {type: integer} args: {type: object} state: {type: string, enum: [queued, leased, done, failed, cancelled, lost]} attempt: {type: integer} cancel_requested: {type: boolean} status: {type: string} result: {type: object} error: {type: object} created_at: {type: string, format: date-time} started_at: {type: string, format: date-time} finished_at: {type: string, format: date-time} JobAnswer: type: object required: [job] properties: job: {$ref: '#/components/schemas/Job'} result: {type: object, description: 'For a check: {exit, verdict, report}.'} JobEvent: type: object properties: seq: {type: integer} at: {type: string, format: date-time} kind: {type: string} msg: {type: string} attrs: {type: object} Plan: type: object properties: account: {type: string} name: {type: string} status: {type: string, enum: [pending, active, suspended]} plan: {type: string, enum: [starter, growth, enterprise]} org_limit: {type: integer} overage_allowed: {type: boolean} billable: {type: integer, description: 'Organizations in use now (a live move or at a customer).'} WebhookEndpoint: type: object properties: url: {type: ['string', 'null']} secret: {type: string, description: '`whs_…`, only when made.'} events: {type: array, items: {type: string}} updated_at: {type: string, format: date-time} WebhookBody: type: object required: [type, id, org, state, at] properties: type: {type: string, enum: [move.state]} id: {type: string, description: 'The move.'} org: {type: integer} tenant: {type: string} external_ref: {type: string} state: {$ref: '#/components/schemas/MoveState'} previous: {type: string} phase: {type: string} error: {type: string} at: {type: string, format: date-time} AlertRule: type: object properties: rule: {type: string} enabled: {type: boolean} threshold: {type: integer} recipients: {type: array, items: {type: string}} summary: {type: string} event: {type: boolean}