API reference

Every endpoint of the SQLly Embedded bridge's management API, generated from its OpenAPI (swagger.json) document, plus the sqlly.app linking endpoints.

Download swagger.json OpenAPI 3.0 version 1.0.0 base http://127.0.0.1:8445

The SQLly bridge in embedded mode exposes this API on its management listener (--manage-bind, default 127.0.0.1:8445). Your web app calls it from its server side to store connection profiles, test connections, mint launch tokens for signed-in users, watch seats, and read or change the license state.

Every /manage/... call needs Authorization: Bearer <manage token> (the bridge's --manage-token, at least 16 characters). Bodies are JSON in and out, at most 8 MB (refused with 413 too_large as soon as the limit is passed). Path segments and query-string values are percent-decoded; an invalid escape is 400 bad_request. Errors are always { "error": "<code>", "message": "<sentence>" }, plus problems when a credential document is invalid. The API sends no CORS headers: keep it on loopback or a private network and never expose it to browsers, because profiles hold database passwords.

The Linking operations are not on the bridge: they are sqlly.app's public endpoints an admin app uses to link an install to a license without copying a key. Their server is sqlly.app.

Guide: sqlly.app/docs/embedded

Conventions

  • Authentication: Authorization: Bearer <manage token> on every /manage/… call. A missing or wrong token is 401 unauthorized. /healthz and the sqlly.app linking endpoints need none.
  • Errors are { "error": "<code>", "message": "<sentence>" }. Branch on error; show message. Invalid credential documents add problems, one string per mistake.
  • Bodies are JSON, at most 8 MB. The limit is enforced while the body arrives: the bridge answers 413 too_large the moment it is passed and never buffers more. A body that takes over 30 seconds to arrive is 408 timeout. Anything unparseable is 400 bad_json, and an unknown route is 404 not_found.
  • Path segments and query values are percent-decoded, so URL-encode user ids and timestamps (DELETE /manage/seats/a%2Fb releases the user a/b). An invalid escape or a result that is not UTF-8 is 400 bad_request.
  • Field names are camelCase in the API's own payloads. Inside a credential document they follow the credential format (sql_auth_accounts, trust_server_certificate).

Endpoints

Health

Liveness, no token needed.

GET /healthz no token

Liveness probe

Answers as soon as the management listener is up. Needs no token; use it from start scripts and service health checks.

200
The management API is listening.
Example 200 response
{
  "ok": true
}

License

The license the bridge runs under: state, check-in, linking and unlinking.

GET /manage/status

Bridge and license status

Everything an admin screen needs: the license state and where it came from, the allowances the signed grant carries, seats in use, open sessions, and the proxy URL browsers use.

200
Current status.
401
unauthorized: the manage token is missing or wrong.
Example 200 response
{
  "version": "1.1.0",
  "instanceId": "b5e1c0f2-7d3a-4c8e-9a61-0f2d4b7e9c13",
  "mode": "connected",
  "license": {
    "state": "ok",
    "source": "linked",
    "licenseId": "emb_Q2x0b3VkU3FsbHkxMjM0NTY3",
    "plan": "embedded-25",
    "planName": "SQLly Embedded 25",
    "status": "active",
    "seats": 25,
    "sessionsPerSeat": 4,
    "instances": 1,
    "domain": "portal.example.com",
    "periodEnd": "2027-10-14T00:00:00+00:00",
    "graceEnd": null,
    "daysRemaining": 365,
    "lastCheckinAt": "2026-10-14T15:00:02Z",
    "staleAt": "2026-10-14T15:40:02Z",
    "licenseFile": null,
    "message": null,
    "fromCache": false,
    "features": [
      "embedded",
      "unlimited-connections",
      "all-engines",
      "test-connection"
    ]
  },
  "seatsUsed": 3,
  "activeSessions": 4,
  "proxyUrl": "wss://portal.example.com/sqlly/proxy",
  "allowLocalhostOrigins": false,
  "storeUrl": "https://sqlly.app",
  "allowFileEngines": false
}
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
POST /manage/license/checkin

Check in with sqlly.app now

Connected and linked licenses: runs a check-in immediately instead of waiting for the 20-minute loop. Offline licenses: re-reads the license file and never touches the network. Either way the reply is the status after the attempt; a failure leaves the state unchanged.

200
Status after the check-in.
401
unauthorized: the manage token is missing or wrong.
409
not_linked: the bridge is unlicensed, there is nothing to check in.
422
license_file_invalid (offline): the file is missing, damaged, or signed for another key.
502
store_unreachable or grant_invalid: sqlly.app could not be reached, or its reply did not verify.
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
Example 409 response
{
  "error": "not_linked",
  "message": "this bridge is not linked to a license yet"
}
PUT /manage/license

Install a linked license key

Gives a bridge that was started without --license-key / --license-file its key at runtime (normally the key your admin app just collected from sqlly.app through the link flow). The bridge checks in with it first and stores it (<state-dir>/license-key, mode 0600) only if sqlly.app answers with a verified grant; from then on it runs as a connected bridge, and the key is reused after restarts. Nothing changes on failure: the key in use keeps serving until the new one has verified. Replacing a linked key with one of another license releases the old license's instance slot after the new key verifies; a regenerated key of the same license reuses the store's instance row.

Request body (LinkLicenseRequest)
{
  "licenseKey": "sqe_2xH7c0VqHk9LmZr1aPq4tN8bW6yD3sJfUe5o"
}
200
Linked. The status now shows license.source: linked.
400
invalid_license_key: empty, malformed, unknown or revoked key.
401
unauthorized: the manage token is missing or wrong.
409
license_from_flag: the bridge was started with --license-key or --license-file, so its license is not managed through this API.
502
store_unreachable: sqlly.app could not be reached or its reply did not verify.
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
DELETE /manage/license

Unlink this install

Releases this bridge's instance slot, revokes its key on sqlly.app (best effort), deletes the stored key, closes every open session and returns the bridge to unlicensed.

204
Unlinked.
401
unauthorized: the manage token is missing or wrong.
404
not_linked: no linked key is stored.
409
license_from_flag: the bridge was started with --license-key or --license-file, so its license is not managed through this API.
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}

Engines

The database engines the credential format accepts, so your UI never hard-codes the list.

GET /manage/engines

Engines the credential format accepts

Every database engine SQLly supports, with the engine id to write in a credential document, its default port, whether server is a file path, and the authentication.type values it accepts.

200
The engines.
401
unauthorized: the manage token is missing or wrong.
Example 200 response
[
  {
    "id": "sql-server",
    "name": "Microsoft SQL Server",
    "defaultPort": 1433,
    "fileBased": false,
    "authentication": [
      "sql-login",
      "integrated",
      "entra-browser",
      "entra-device-code"
    ]
  },
  {
    "id": "postgres",
    "name": "PostgreSQL",
    "defaultPort": 5432,
    "fileBased": false,
    "authentication": [
      "sql-login"
    ]
  },
  {
    "id": "sqlite",
    "name": "SQLite",
    "defaultPort": 0,
    "fileBased": true,
    "authentication": [
      "none"
    ]
  }
]
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}

Profiles

A profile is a named SQLly credential document: the connections (and labels) one group of users sees. The bridge is the store of record; secrets never reach the browser.

GET /manage/profiles

List profiles

Every stored profile without its credentials.

200
The profiles.
401
unauthorized: the manage token is missing or wrong.
Example 200 response
[
  {
    "id": "analysts",
    "name": "Analysts",
    "connections": 2,
    "updatedAt": "2026-10-14T15:02:11Z"
  }
]
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
GET /manage/profiles/{id}

Read one profile

The profile with its full credential document, secrets included: the caller is your trusted server-side app.

id
path · string matching ^[A-Za-z0-9_-]{1,64}$ · required - Your id for the profile.
200
The profile.
400
invalid_profile_id: ids are 1-64 of A-Z a-z 0-9 _ -.
401
unauthorized: the manage token is missing or wrong.
404
unknown_profile: no profile has that id.
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
Example 404 response
{
  "error": "unknown_profile",
  "message": "no profile with id analysts"
}
PUT /manage/profiles/{id}

Create or replace a profile

Stores the profile under id (create or replace). The credential document is validated in full before anything is written: every problem the parser finds comes back in problems, and an invalid document changes nothing. A document holding a file-based engine (SQLite, DuckDB) is refused the same way unless the bridge runs with --allow-file-engines true, because such a connection gives its users file access on the bridge host. Sessions already open keep the catalog they started with; edits apply to the next launch.

id
path · string matching ^[A-Za-z0-9_-]{1,64}$ · required - Your id for the profile.
Request body (PutProfileRequest)
{
  "name": "Analysts",
  "credentials": {
    "format": "sqlly-credentials",
    "version": 1,
    "clients": [
      {
        "name": "Northwind Traders",
        "color": "#1E88E5"
      }
    ],
    "environments": [
      {
        "name": "Production",
        "color": "#C62828",
        "safety": {
          "only_allow_select": true
        }
      }
    ],
    "sql_auth_accounts": [
      {
        "name": "Reader",
        "username": "reader",
        "password": "…"
      }
    ],
    "connections": [
      {
        "name": "Northwind",
        "engine": "sql-server",
        "server": "sql01.internal",
        "database": "Northwind",
        "environment": "production",
        "client": "Northwind Traders",
        "authentication": {
          "type": "sql-login",
          "account": "Reader"
        },
        "tls": {
          "encrypt": true,
          "trust_server_certificate": true
        }
      },
      {
        "name": "Warehouse",
        "engine": "postgres",
        "server": "dw.internal",
        "database": "dw",
        "environment": "production",
        "client": "Northwind Traders",
        "authentication": {
          "type": "sql-login",
          "account": "Reader"
        },
        "tls": {
          "mode": "verify-full"
        }
      }
    ]
  }
}
200
Stored.
400
invalid_profile (with problems: parser problems, or each file-based engine while --allow-file-engines is off), or invalid_profile_id.
401
unauthorized: the manage token is missing or wrong.
500
storage: the state directory could not be written.
Example 400 response
{
  "error": "invalid_profile",
  "message": "the credential document has 2 problems",
  "problems": [
    "connections[0].engine: unknown engine \"postgress\"",
    "connections[1].authentication.pasword: unknown key"
  ]
}
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
DELETE /manage/profiles/{id}

Delete a profile

Removes the profile. Open sessions keep the catalog they started with; new launch tokens for it are refused with unknown_profile.

id
path · string matching ^[A-Za-z0-9_-]{1,64}$ · required - Your id for the profile.
204
Deleted.
400
invalid_profile_id: ids are 1-64 of A-Z a-z 0-9 _ -.
401
unauthorized: the manage token is missing or wrong.
404
unknown_profile: no profile has that id.
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
Example 404 response
{
  "error": "unknown_profile",
  "message": "no profile with id analysts"
}

Launch

Launch tokens open SQLly for one user with one profile. Seats are concurrent users.

POST /manage/tokens

Mint a launch token

Creates a short-lived token that opens SQLly for user with profile. Put the returned proxyUrl and token into the page that loads SQLly (globalThis.SQLLY_PROXY_URL / globalThis.SQLLY_PROXY_TOKEN). The token can be reused until it expires (page reloads), and the profile is captured at mint time.

A user holds a seat while they have an open session or an unexpired token. Minting for a user who already holds a seat never fails on the seat count; minting for a new user fails with seat_limit when every seat of the license is in use, counting users on every bridge of the license.

Request body (MintTokenRequest)
{
  "user": "dana.analyst",
  "profile": "analysts",
  "ttlSeconds": 600
}
201
The token.
400
invalid_user (empty or contains whitespace, at most 128 characters), or invalid_profile (the stored profile no longer validates, or holds a file-based engine while --allow-file-engines is off).
401
unauthorized: the manage token is missing or wrong.
404
unknown_profile: no profile has that id.
409
seat_limit: every seat is in use. Release one (DELETE /manage/seats/{user}) or buy a larger plan.
503
license_not_ok: the license is not ok or grace (stale, expired, revoked, not admitted, domain mismatch, or unlicensed). The message says which.
Example 201 response
{
  "token": "elt_q3J0n8w2Zx5rVb1kTq9yLs4dHf7cMa6p",
  "expiresAt": "2026-10-14T15:12:11Z",
  "proxyUrl": "wss://portal.example.com/sqlly/proxy",
  "user": "dana.analyst",
  "profile": "analysts",
  "seatsUsed": 3,
  "seats": 25
}
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
GET /manage/seats

Seats in use

Users holding a seat on this bridge, with their open sessions and live tokens. seatsUsed also counts users sqlly.app reported on other bridges of the same license.

200
Seat usage.
401
unauthorized: the manage token is missing or wrong.
Example 200 response
{
  "seats": 25,
  "seatsUsed": 2,
  "users": [
    {
      "user": "dana.analyst",
      "activeSessions": 1,
      "tokens": 1,
      "since": "2026-10-14T15:02:11Z"
    },
    {
      "user": "sam.support",
      "activeSessions": 2,
      "tokens": 1,
      "since": "2026-10-14T14:40:03Z"
    }
  ]
}
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}
DELETE /manage/seats/{user}

Release a user's seat

Revokes the user's tokens and closes their sessions, freeing the seat at once. Call it when a user signs out of your app or loses access. Releasing a user who holds no seat is not an error. The path segment is percent-decoded: a%2Fb releases the user a/b.

user
path · string · required - The user id you minted tokens for (URL-encoded).
204
Released.
400
bad_request: the segment is not valid percent-encoding.
401
unauthorized: the manage token is missing or wrong.
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}

Connections

Prove a connection works before anyone uses it.

POST /manage/test-connection

Test one connection

Runs a throw-away engine against a credential document holding exactly one connection and executes a trivial query. Connection failures are reported as ok: false with the engine's own message (HTTP 200); only an unusable document is an HTTP error.

Request body (TestConnectionRequest)
{
  "credentials": {
    "format": "sqlly-credentials",
    "version": 1,
    "connections": [
      {
        "name": "Northwind",
        "engine": "sql-server",
        "server": "sql01.internal",
        "database": "Northwind",
        "authentication": {
          "type": "sql-login",
          "username": "reader",
          "password": "…"
        },
        "tls": {
          "encrypt": true,
          "trust_server_certificate": true
        }
      }
    ]
  }
}
200
The outcome, success or not.
400
invalid_profile: the document is invalid, does not hold exactly one connection, or holds a file-based engine while --allow-file-engines is off (see problems).
401
unauthorized: the manage token is missing or wrong.
Example 200 response
{
  "ok": true,
  "elapsedMs": 212,
  "message": "Connected: SQL Server 17.0"
}
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}

Audit

The query audit trail: every query and row edit a session ran, who ran it, against which connection, and how it ended. Kept in memory and in <state-dir>/queries.jsonl (SQL text included) unless the bridge runs with --query-log false.

GET /manage/queries

Query audit trail

Every query and row edit the bridge's sessions ran, newest first: the user and profile of the launch token, the connection it named (name, server, engine, database), the SQL text (cut to 64 KiB, see sqlTruncated), when it started, its status and duration, and the engine's error when it failed. Requests still running are listed with status running; a request refused because the license was not in order is failed with the license message; whatever was running when a session closed is cancelled with error session closed.

The bridge keeps the newest --query-log-max-entries entries (20 000 by default) in memory and in <state-dir>/queries.jsonl, so the trail survives a restart. With --query-log false nothing is recorded and the reply is { "enabled": false, "queries": [] }.

limit
query · integer - Entries to return, clamped to 1-1000.
user
query · string - Only this user id (exact match, URL-encoded).
profile
query · string - Only this profile id (exact match).
since
query · string (date-time) - Only entries whose at is at or after this RFC 3339 timestamp (URL-encoded).
status
query · QueryStatus - Only entries in this status.
200
The matching entries, newest first.
400
bad_request: limit is not a whole number, since is not RFC 3339, status is unknown, or a value is not valid percent-encoding.
401
unauthorized: the manage token is missing or wrong.
Example 200 response
{
  "enabled": true,
  "total": 2,
  "limit": 200,
  "queries": [
    {
      "id": "q-7",
      "sessionId": 12,
      "kind": "query",
      "at": "2026-10-14T15:04:09.120Z",
      "user": "dana.analyst",
      "profile": "analysts",
      "profileName": "Analysts",
      "connection": "Northwind",
      "server": "sql01.internal",
      "database": "Northwind",
      "engine": "Microsoft SQL Server",
      "sql": "SELECT TOP 100 * FROM Orders",
      "sqlTruncated": false,
      "status": "completed",
      "durationMs": 184,
      "error": null
    },
    {
      "id": "q-6",
      "sessionId": 12,
      "kind": "query",
      "at": "2026-10-14T15:03:51.004Z",
      "user": "dana.analyst",
      "profile": "analysts",
      "profileName": "Analysts",
      "connection": "Northwind",
      "server": "sql01.internal",
      "database": "Northwind",
      "engine": "Microsoft SQL Server",
      "sql": "SELECT * FROM Ordres",
      "sqlTruncated": false,
      "status": "failed",
      "durationMs": 31,
      "error": "Invalid object name 'Ordres'."
    }
  ]
}
Example 401 response
{
  "error": "unauthorized",
  "message": "management token missing or wrong"
}

Linking

sqlly.app endpoints (server sqlly.app) for linking an install to a license: create a code, the owner approves it in their browser, collect the install's own key once, then hand it to the bridge with PUT /manage/license.

GET /api/embedded/public-key sqlly.app no token

License-signing public key (sqlly.app)

The PEM public key sqlly.app signs license grants with, so an operator can compare it with the key compiled into the bridge.

200
PEM text.
503
signing_not_configured.

Schemas

The objects above, as defined in swagger.json. A ? after a type means it can be null.

Error

Every error reply. error is a stable machine code; message is a sentence you can show an admin.

FieldTypeDescription
error * string
message * string
problems string[] Present for invalid_profile: every problem the credential parser found.

LicenseState

ok and grace serve queries; every other state refuses work with a message SQLly shows its users.

One of: ok grace stale not_admitted expired revoked domain_mismatch unlicensed

StatusReport

FieldTypeDescription
version string Bridge version.
instanceId string This bridge process's id, as sqlly.app sees it.
mode connected | offline
license LicenseReport
seatsUsed integer Seats in use across every bridge of the license.
activeSessions integer Open SQLly sessions on this bridge.
proxyUrl string The WebSocket URL browsers use (--public-url).
allowLocalhostOrigins boolean
storeUrl string
allowFileEngines boolean Whether profiles may hold file-based engines (--allow-file-engines).

LicenseReport

Allowances come from the signed grant, never from unsigned data.

FieldTypeDescription
state LicenseState
source flag | file | linked | none flag: --license-key; file: --license-file; linked: stored by PUT /manage/license; none: unlicensed.
licenseId string?
plan string? Plan id, or custom for a license issued with its own terms.
planName string?
status string? sqlly.app's status from the last check-in: active, grace, expired, revoked, domain_mismatch.
seats integer Concurrent users the license allows.
sessionsPerSeat integer Open SQLly tabs one user may hold.
instances integer Bridges that may run at once (connected licenses; 0 offline).
domain string? The bound domain; browsers must come from it or a subdomain.
periodEnd string? End of the paid period (ISO 8601).
graceEnd string? Set while in grace: when the bridge stops serving.
daysRemaining integer? Whole days until the bridge stops serving.
lastCheckinAt string (date-time)?
staleAt string (date-time)? When the bridge goes stale if no check-in succeeds (last good grant + 40 minutes).
licenseFile string? Offline: the license file in use.
message string? The sentence users see when something is wrong (also grace countdowns).
fromCache boolean True when the bridge started from its cached grant because sqlly.app was unreachable.
features string[]

LinkLicenseRequest

FieldTypeDescription
licenseKey * string A key starting sqe_.

EngineInfo

FieldTypeDescription
id string The engine value for a credential document.
name string
defaultPort integer
fileBased boolean server is a file path and no sign-in applies.
authentication string[] Accepted authentication.type values.

ProfileSummary

FieldTypeDescription
id string
name string
connections integer Number of connections in the document.
updatedAt string (date-time)

StoredProfile

FieldTypeDescription
id string
name string
credentials CredentialDocument
updatedAt string (date-time)

PutProfileRequest

FieldTypeDescription
name * string Display name, shown to users in SQLly.
credentials * CredentialDocument

CredentialDocument

A SQLly credential document (the same format as SQLly's File → Import Connections). Unknown keys are errors, so typos are reported instead of ignored. Full reference: sqlly.app/docs (Credential file format).

FieldTypeDescription
format * sqlly-credentials
version * 1
clients Label[]
projects Label[]
environments Label[]
sql_auth_accounts SharedLogin[]
tunnel_profiles object[]
connections Connection[]

Label

FieldTypeDescription
name * string
color string #RGB, #RRGGBB or #RRGGBBAA.
image string Icon as a data: URI or bare base64 (PNG, JPEG, GIF, WebP, BMP, ICO, SVG; up to 1 MB).
safety object Only the flags you list change.

SharedLogin

FieldTypeDescription
name * string
username * string
password string

Connection

FieldTypeDescription
name * string
engine * string An id from GET /manage/engines.
server * string Host, IP, host\instance, or a file path for SQLite/DuckDB.
port integer
database string
environment string local, dev, test, staging, production, or unset.
client string A client label name.
project string A project label name.
authentication Authentication
tls object
timeout_seconds integer
statement_timeout_seconds integer
application_name string
safety object
favorite boolean
startup_sql string
tunnel_profile string
transport object Inline proxy, ssh and kube hops.
masking object { include: [...], exclude: [...] } column masking rules.

Authentication

FieldTypeDescription
type * sql-login | integrated | entra-browser | entra-device-code | aws-rds-iam | gcp-cloud-sql-iam | auth-token | none
username string
password string
account string Name of a shared login in sql_auth_accounts.
region string
profile string
token string

MintTokenRequest

FieldTypeDescription
user * string Your stable id for the user (no whitespace). Seats are counted per distinct user.
profile * string A profile id.
ttlSeconds integer Token lifetime; defaults to --token-ttl-seconds (600) and is clamped to 10 s - 24 h.

MintedToken

FieldTypeDescription
token string
expiresAt string (date-time)
proxyUrl string
user string
profile string
seatsUsed integer Seats in use including this user.
seats integer

SeatsReport

FieldTypeDescription
seats integer
seatsUsed integer
users UserSeat[]

UserSeat

FieldTypeDescription
user string
activeSessions integer
tokens integer Unexpired launch tokens.
since string (date-time)

TestConnectionRequest

FieldTypeDescription
credentials * CredentialDocument

TestOutcome

FieldTypeDescription
ok boolean
elapsedMs integer
message string The engine's own message, success or failure.

QueryStatus

running until the engine answers; then completed, failed (see error) or cancelled (by the user, or session closed).

One of: running completed failed cancelled

QueryEntry

One audited request.

FieldTypeDescription
id string The engine request id, unique within its session.
sessionId integer The bridge's id for the browser session.
kind query | row-edit
at string (date-time) When the request reached the bridge.
user string The launch token's user id.
profile string The launch token's profile id.
profileName string
connection string The connection's name in the profile.
server string
database string? The profile's database when it pins one, else the database the request named.
engine string The engine's display name.
sql string The SQL text, or row edit: <op> for a row edit. Cut to 64 KiB.
sqlTruncated boolean
status QueryStatus
durationMs integer? Null while running.
error string? The engine's error for failed; session closed for a request cut off by its session ending.

QueryListing

FieldTypeDescription
enabled boolean False when the bridge runs with --query-log false; then queries is always empty.
queries QueryEntry[] Newest first.
total integer Entries matching the filters before limit was applied.
limit integer The page size applied (1-1000).

CreateLinkRequest

FieldTypeDescription
name string Shown to the owner on the approval page and on their Embedded page.
hostname string
appVersion string

CreateLinkResponse

FieldTypeDescription
linkId string
code string Show this to the owner so they can check it matches.
secret string Keep server-side; it collects the key.
linkUrl string Open this for the owner to approve.
expiresAt string (date-time)
pollIntervalSeconds integer

LinkTokenRequest

FieldTypeDescription
linkId * string
secret * string

LinkPending

FieldTypeDescription
status pending
expiresAt string (date-time)

LinkApproved

FieldTypeDescription
status approved
licenseKey string Returned exactly once. Pass it to PUT /manage/license.
keyId string
licenseId string
plan string Plan id, or custom.
planName string
accountEmail string

* required