Vendor API reference
This page documents the current MVP contract for products and licences. All paths below are relative to https://sealbind.io/api/v1.
Bearer authentication
Send the vendor API key in every request as Authorization: Bearer YOUR_API_KEY. Keep it on your server: it can create products, issue licences, and change licence state.
curl -s https://sealbind.io/api/v1/products \
-H "Authorization: Bearer $SEALBIND_API_KEY"Missing, unknown, and revoked keys return HTTP 401 with one of these exact bodies:
{ "error": "missing_api_key" }
{ "error": "invalid_api_key" }
{ "error": "revoked_api_key" }Products
Product licenseType is PERPETUAL, SUBSCRIPTION, or TRIAL. Names are trimmed. Device limits and supplied duration values must be positive integers. A duration is required by licence issuance for non-perpetual products; perpetual licences ignore it when calculating expiry.
Create — POST /products
curl -s -X POST https://sealbind.io/api/v1/products \
-H "Authorization: Bearer $SEALBIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Desktop Pro","maxDevicesPerLicense":2,"licenseType":"SUBSCRIPTION","licenseDurationDays":365}'{
"product": {
"id": "clx0product0000000000000",
"name": "Desktop Pro",
"accountId": "clx0account00000000000000",
"maxDevicesPerLicense": 2,
"licenseType": "SUBSCRIPTION",
"licenseDurationDays": 365,
"createdAt": "2026-08-08T12:00:00.000Z",
"updatedAt": "2026-08-08T12:00:00.000Z"
}
}Invalid JSON returns 400 {"error":"invalid_json"}; a non-object body returns400 {"error":"invalid_body"}. Field validation returns400 with {"error":"invalid_product_input","issues":["..."]}. The account product limit returns 402 with{"error":"product_limit_exceeded","maxProducts":3}.
List — GET /products
curl -s https://sealbind.io/api/v1/products \
-H "Authorization: Bearer $SEALBIND_API_KEY"{ "products": [ /* product objects in the shape above */ ] }Get — GET /products/:id
curl -s https://sealbind.io/api/v1/products/clx0product0000000000000 \
-H "Authorization: Bearer $SEALBIND_API_KEY"{ "product": { /* product object */ } }Update — PATCH /products/:id
Send any subset of the create fields. Set licenseDurationDays to null to clear it.
curl -s -X PATCH https://sealbind.io/api/v1/products/clx0product0000000000000 \
-H "Authorization: Bearer $SEALBIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Desktop Pro 2","maxDevicesPerLicense":3}'{ "product": { /* updated product object */ } }Delete — DELETE /products/:id
curl -s -X DELETE https://sealbind.io/api/v1/products/clx0product0000000000000 \
-H "Authorization: Bearer $SEALBIND_API_KEY"Success is HTTP 204 with no response body. Get, update, and delete return404 {"error":"product_not_found"} when the product is absent or belongs to another account.
Licences
Issue — POST /licenses
metadata is optional and, when present, must be a JSON object.
curl -s -X POST https://sealbind.io/api/v1/licenses \
-H "Authorization: Bearer $SEALBIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"productId":"clx0product0000000000000","metadata":{"customerId":"cus_123"}}'{
"license": {
"id": "clx0license0000000000000",
"key": "ABCD-EFGH-JKLM-NPQR",
"status": "ACTIVE",
"metadata": { "customerId": "cus_123" },
"expiresAt": "2027-08-08T12:00:00.000Z",
"productId": "clx0product0000000000000",
"createdAt": "2026-08-08T12:00:00.000Z",
"updatedAt": "2026-08-08T12:00:00.000Z",
"product": {
"id": "clx0product0000000000000",
"name": "Desktop Pro",
"accountId": "clx0account00000000000000",
"licenseType": "SUBSCRIPTION",
"licenseDurationDays": 365
}
}
}Perpetual licences have expiresAt: null. Bad input returns 400 with{"error":"invalid_license_input","issues":["..."]}; an unavailable product returns404 {"error":"product_not_found"}; the active-licence plan limit returns402 with {"error":"license_limit_exceeded","maxActiveLicenses":100}.
List — GET /licenses; get — GET /licenses/:key
curl -s https://sealbind.io/api/v1/licenses \
-H "Authorization: Bearer $SEALBIND_API_KEY"{ "licenses": [ /* licence objects in the shape above, newest first */ ] }curl -s https://sealbind.io/api/v1/licenses/ABCD-EFGH-JKLM-NPQR \
-H "Authorization: Bearer $SEALBIND_API_KEY"{ "license": { /* licence object */ } }Get returns 404 {"error":"license_not_found"}. List and get refresh a passed expiry to status EXPIRED.
Activate — POST /licenses/:key/activate
curl -s -X POST https://sealbind.io/api/v1/licenses/ABCD-EFGH-JKLM-NPQR/activate \
-H "Authorization: Bearer $SEALBIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fingerprint":"device-a"}'{
"activation": {
"id": "clx0activation00000000000",
"fingerprint": "<sha256 hex digest>",
"licenseId": "clx0license0000000000000",
"activatedAt": "2026-08-08T12:00:00.000Z",
"lastSeenAt": "2026-08-08T12:00:00.000Z",
"deactivatedAt": null
},
"token": "<signed activation token>"
}Repeating activation for the same normalised fingerprint updates lastSeenAt rather than consuming another slot. Errors are 400 invalid_fingerprint, 404 license_not_found,409 device_limit_exceeded, or 422 license_unavailable, each as {"error":"CODE"}.
Deactivate — POST /licenses/:key/deactivate
curl -s -X POST https://sealbind.io/api/v1/licenses/ABCD-EFGH-JKLM-NPQR/deactivate \
-H "Authorization: Bearer $SEALBIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fingerprint":"device-a"}'{ "activation": { /* activation object with deactivatedAt set */ } }Errors are 400 invalid_fingerprint or 404 license_not_found/activation_not_found.
Validate — POST /licenses/:key/validate
curl -s -X POST https://sealbind.io/api/v1/licenses/ABCD-EFGH-JKLM-NPQR/validate \
-H "Authorization: Bearer $SEALBIND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fingerprint":"device-a"}'{
"result": "VALID",
"license": {
"key": "ABCD-EFGH-JKLM-NPQR",
"status": "ACTIVE",
"licenseType": "SUBSCRIPTION",
"productId": "clx0product0000000000000",
"maxDevices": 2,
"expiresAt": "2027-08-08T12:00:00.000Z"
},
"token": "v1.<payload>.<signature>"
}result is VALID, INVALID, EXPIRED, REVOKED, orDEVICE_LIMIT_EXCEEDED. These licence outcomes still use HTTP 200. Only VALID includes a validation token. Request errors are 400 {"error":"invalid_fingerprint"} and 404 {"error":"license_not_found"}.
Revoke — POST /licenses/:key/revoke
curl -s -X POST https://sealbind.io/api/v1/licenses/ABCD-EFGH-JKLM-NPQR/revoke \
-H "Authorization: Bearer $SEALBIND_API_KEY"{ "license": { /* licence object with status "REVOKED" */ } }Revocation is idempotent. An unknown or other-account key returns 404 {"error":"license_not_found"}.
Device fingerprint privacy and normalisation
Generate a stable, non-empty device identifier in your client. SealBind trims it, lowercases it, and stores a SHA-256 hex digest; therefore " Device-A " and "device-a" address the same activation. Do not put names, email addresses, serial numbers, or other directly identifying data in the value. Hashing reduces accidental disclosure but does not make a low-entropy identifier anonymous; use an app-scoped random identifier where possible. Send the same raw value to activate, validate, and deactivate.
Validation token TTL and authority
A successful online validation issues an Ed25519-signed v1 token with iat and exp claims. Its TTL is exactly 300 seconds (five minutes), including for perpetual licences. Offline verification proves signature and token expiry only; it cannot observe a revocation, deactivation, changed device count, or licence state change made after issuance. Treat POST /licenses/:key/validate as authoritative and use the short-lived token only for bounded offline operation.
See Offline verification for the token format and public-key verification example.