Skip to content
DockBoard
Browse the documentation
API

The licensing API

Activate a license, send a heartbeat, and fetch the public keys used to verify a license token.

This is the API of this site — the one that issues and verifies licenses. It is separate from the API of your own instance, and you rarely call it by hand: DockBoard activates and renews itself.

It matters in one situation: you sell or provision DockBoard for other people, and want a machine to come up already licensed with no human step in the middle.

The surfaces

PrefixWho calls itAuth
/api/v1/license/*DockBoard instances, machine to machineThe license key itself, on a signed request
/api/admin/*The back-office, and provisioning integrationsA staff session — or a machine token where a handler was opened to one
/api/shop/*Customers, in their account areaA customer session

This page is about the second one.

Machine tokens

A machine token is a bearer secret with no session, no second factor, and no human at the other end. It goes in a CI variable, a provider’s control panel, a cloud-init script. Mint it in Admin → Tokens, or over the API with a staff session:

BASH
curl -X POST https://licenses.example.com/api/admin/machine-tokens \
  -H "Authorization: Bearer $STAFF_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "OVH provisioning",
    "permissions": [
      "customers:view", "customers:manage",
      "subscriptions:view", "subscriptions:manage",
      "licenses:view", "licenses:manage",
      "plans:view", "instances:view", "instances:manage"
    ]
  }'
The secret appears in that response and nowhere else. Only sha256(secret) is stored; no endpoint re-serves it and no support process can recover it.

Unknown and denylisted permissions are dropped at mint; if nothing valid survives, the mint is refused rather than producing a weaker token than you asked for. The token routes themselves are session-only, for the same reason key management is: a token that mints tokens outlives its own revocation.

What a machine token can never hold

DeniedBecause
staff:view staff:manageReading or editing staff is how a leaked token mints itself a human SUPERADMIN and stops being a token problem.
settings:view settings:manageSigning-key rotation lives here — rotate the key and every license in the field is re-signed by someone else’s. So does token minting.
payments:manageRefunds and credit adjustments move real money. payments:view stays grantable — reading a payment history is ordinary provisioning work; moving money is a human act.

The denylist is enforced in three places, not one: at mint, at resolve, and at the door on every request. A token predating a denylist change gains nothing from it.

What bounds a token

A machine token holds no session to expire and no human to notice it is still there. Three limits stand in for the attention it will never get.

An allowlist of source addresses
allowedIps — one IP or CIDR range per entry, both families. Present it from anywhere else and it is refused. Matched fail-closed: a call whose source address cannot be determined is refused too, because an implementation that waved those through would silently disable the control everywhere at once. A malformed rule is a 400 at mint rather than a lockout at 3am, and 0.0.0.0/0 is refused outright — it reads as a restriction and behaves as none. Leave it empty and the token is reachable from anywhere, which is the default.
An expiry that means something
Optional in general — a provider’s provisioning credential is meant to outlive a rotation window — but mandatory, and capped at 90 days, for `versions:manage`: that token blesses the release every install auto-updates to, and an immortal one sitting in a CI secret store is the highest-value target on the platform. Prefer GitHub OIDC, which stores no secret at all. Any expiry you do set must be in the future and within 365 days: a past date mints a credential dead on arrival, and 2099-01-01 is “never” written so it reads as a rotation policy.
A ceiling on how many exist
50 live tokens per install. Revoked and expired rows do not count — they are audit history, and counting them would wedge an install that rotates on schedule, which is exactly the behaviour the 90-day rule above exists to force.
JSON
{
  "name": "OVH provisioning",
  "permissions": ["licenses:manage", "customers:manage"],
  "allowedIps": ["203.0.113.0/24", "2001:db8::/32"],
  "expiresAt": "2027-02-17T00:00:00.000Z"
}

The listing in Admin → Tokens shows each token’s allowlist, and lastUsedIp beside lastUsedAt — “where is this being used from?” is the question you have when you find a token nobody recognises, and it was the one the listing could not answer.

Provisioning: one call, one licensed machine

BASH
curl -X POST https://licenses.example.com/api/admin/provisioning \
  -H "Authorization: Bearer $DOCKBOARD_MACHINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "customer@example.com",
    "planSlug": "pro",
    "seats": 1,
    "reference": "order-12345"
  }'
JSON
{
  "created": true,
  "customer":     { "id": "…", "email": "customer@example.com", "created": true },
  "subscription": { "id": "…", "planSlug": "pro", "seats": 1, "status": "ACTIVE" },
  "license":      { "id": "…", "keyId": "lic_…", "expiresAt": "2027-08-17T…" },
  "licenseKey":   "eyJ…",
  "installCommand": "curl -fsSL https://get.dockboard.io/install.sh | sudo DOCKBOARD_LICENSE_KEY='eyJ…' sh"
}

Hand installCommand to cloud-init and the customer’s box comes up already licensed — they never see Admin → License. See installing DockBoard for what happens on the receiving end.

FieldRequiredNotes
emailyesThe end customer. An existing account is reused, never modified — not renamed, not re-passworded, not re-roled.
planSlugyesBy slug (pro), not id — a slug survives a database restore where an id does not. An inactive plan is refused.
seatsnoDefault 1.
billingCyclenoMONTHLY or YEARLY.
statusnoDefault `ACTIVE`, not TRIALING — a provisioning call is made because the customer has already paid the provider.
currentPeriodEndnoISO-8601. The license expires with it; absent, the default of one year applies.
maxActivationsnoDefaults to the plan’s cap, raised to the subscription’s previous cap if higher, never lowered.
referencenoIdempotency key. See below.

`reference` is the whole design

The three steps this call composes — account, subscription, license — are each available on their own. They are fused here because the interesting part is not any one of them; it is that the sequence must be safe to retry.

A webhook that redelivers. A pipeline step that re-runs. A control panel where someone clicks twice. Without a key to recognise the repeat by, each of those bills the customer for a second subscription and mints a second license — and because issuing revokes the previous active one, the customer’s running install is then silently on a revoked key.

  • The replay check runs first, before anything is created, so a retry is a pure read.
  • It returns the subscription’s current active license, not the one that call originally minted — a plan change or a renewal supersedes the old key, and a provider re-reading their reference must get the key that works today.
  • created: false in the response says a replay happened, and the audit trail records it as a replay rather than a provision.
One replay case is deliberately an error: a reference whose subscription has no active license — all revoked, a cancelled subscription — answers 400. Re-provisioning it would resurrect access someone deliberately withdrew.

What a token can reach

AreaBase pathPermissions
ProvisioningPOST /api/admin/provisioninglicenses:manage
Customers/api/admin/customers — list, read, create, set status, reset passwordcustomers:view / customers:manage
Subscriptions/api/admin/subscriptions — list, read, create, update, change preview, cancel, payment historysubscriptions:*, payments:view
Licenses/api/admin/licenses — list, read, read the key, issue, revoke, suspend, resume, extend, reissuelicenses:view / licenses:manage
Instances/api/admin/instances — list, read, deactivate, reactivateinstances:view / instances:manage
Plans and features/api/admin/plans, /api/admin/featuresplans:*, features:*

Two entries deserve a note. GET /api/admin/licenses/:id/key returns the signed license token itself, and is open to machine tokens on purpose — a provisioning call that cannot read back the key it just issued cannot install anything. Every read is audited, for a session and a token alike, which is the control that makes it safe. And POST /api/admin/instances/:id/deactivate is what frees the activation slot a rebuilt VM still holds: without it, a provider that recycles machines has customers whose next install is refused for a server that no longer exists.

Anything outside that table — staff, settings, signing keys, refunds — answers a token with:

JSON
{
  "statusCode": 403,
  "message": "This endpoint is not available to machine tokens. Use a staff session."
}

Errors

StatusMeans
401 “Invalid or expired machine token”Unknown, revoked, or expired. One message for all three — the differences are exactly what a prober would use to map the token space.
403 “not available to machine tokens”The handler is session-only.
403 “may never hold X”The handler’s permission is on the denylist.
400 “Plan … is not active”Selling a withdrawn plan through a provider integration is how a retired price list stays alive for years.
400 “Reference … has no active license”The replay target was cancelled. Re-activate the subscription, or provision with a new reference.

Operating a machine token follows the same rules as operating an API key: rotate by overlap, treat lastUsedAt as a liveness signal rather than an access log, and on a leak revoke first and read the audit trail second.

The licensing API — DockBoard