License API reference
Complete request and response contract for all four license endpoints.
Four endpoints, all under /api/v1, all POST, all rate limited to 60 requests per minute. No authentication token — the license key is the credential.
POST /api/v1/license/verify#
The endpoint your software calls to check entitlement. If a domain is supplied it is registered as a side effect, so most integrations never need to call anything else.
Parameters#
| Parameter | Required | Notes |
|---|---|---|
license_key |
yes | The customer’s key. |
domain |
no | Up to 255 characters. Supplying it activates the domain, or refreshes its last-seen timestamp if already active. |
product_slug |
no | Up to 255 characters. When present, the key must belong to that product or the request fails. |
current_version |
no | Up to 64 characters. The version currently installed. |
Request#
curl -X POST https://your-panel.example/api/v1/license/verify
-H "Content-Type: application/json"
-H "Accept: application/json"
-d '{
"license_key": "MP-XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"domain": "customer-site.com",
"product_slug": "my-plugin"
}'
Success — 200#
{
"valid": true,
"license_key": "MP-XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"status": "active",
"plan": "Agency",
"update_entitled": true,
"update_access_until": "2027-03-14",
"update_expires_at": "2027-03-14T00:00:00.000000Z",
"domain_limit": 5,
"domains_used": 2,
"domains_remaining": 3,
"activated_domains": ["customer-site.com", "another-site.com"]
}
Response fields#
| Field | Meaning |
|---|---|
valid |
The key exists and the subscription is active. |
status |
One of active, canceled, past_due, paused. |
plan |
Plan name, useful for unlocking tier-specific features. |
update_entitled |
Whether the update window is still open. |
update_access_until |
Update expiry as a plain date. |
update_expires_at |
The same moment as a full timestamp. |
domain_limit |
Concurrent domain cap, or null when unlimited. |
domains_used |
How many are currently active. |
domains_remaining |
Slots left, or null when unlimited. |
activated_domains |
Every currently active domain. |
Failure — 403#
{
"valid": false,
"reason": "invalid_or_inactive"
}
reason |
Meaning |
|---|---|
invalid_or_inactive |
No such key, or the subscription is not active. |
product_mismatch |
The key is real but belongs to a different product than the product_slug sent. |
domain_limit_exceeded |
The key is valid but every domain slot is already taken. |
domain_invalid |
The domain could not be parsed. |
POST /api/v1/license/activate#
Behaves identically to verify and returns the same payload. It exists so integration code can read clearly: call activate when the customer first enters their key, verify on every later check.
POST /api/v1/license/update-check#
A narrower answer to one question: may this installation take a newer build?
Takes the same parameters as verify.
Entitled — 200#
{
"valid": true,
"update_available": true,
"update_expires_at": "2027-03-14T00:00:00.000000Z"
}
Window closed — 200#
{
"valid": true,
"update_available": false,
"reason": "update_window_expired",
"update_expires_at": "2026-03-14T00:00:00.000000Z"
}
Note the status is 200 and valid is still true. A lapsed customer has a working license — they are simply not entitled to newer releases. An invalid key returns the 403 shape above instead.
POST /api/v1/license/download#
Streams a release archive, so your software can update itself without sending the customer to the panel.
Parameters#
| Parameter | Required | Notes |
|---|---|---|
license_key |
yes | The customer’s key. |
version |
one of the two | Version number, such as 1.4.2. |
version_id |
one of the two | The version’s identifier, if you already have it. |
domain |
no | Activated or refreshed as with verify. |
Success#
The archive itself, as a file stream. The download is recorded against the subscription, version, user, IP, and user agent.
Failures#
Errors return a JSON body with a message field, not the reason shape used by the other endpoints.
| Status | Condition |
|---|---|
| 403 | Invalid or inactive subscription. |
| 403 | The update window has expired. |
| 403 | Domain limit exceeded. |
| 403 | The requested version was released after the update window closed. |
| 404 | No such version for this product. |
| 404 | The archive is missing from storage. |
Integration notes#
- Cache the result. Verify once or twice a day, not on every page load. The rate limit is 60 per minute per client and a busy site will hit it.
- Fail open on network errors. Treat a timeout as “unknown”, not as “invalid”. Your customer’s site must not break because your panel had an outage.
- Fail closed only on an explicit 403. That is the panel actually telling you the key is not good.
- Send
product_slugso a key for one of your products cannot be used to unlock another. - Skip local hostnames so staging copies do not eat domain slots.
- Show the customer the reason.
domain_limit_exceededandinvalid_or_inactiveneed very different messages, and surfacing the difference prevents most support tickets.