Preview. The SealBind API is in active development. The v1 shapes below are the intended contract; details may change before general availability.

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.

request
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:

authentication errors
{ "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

request
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}'
201 response
{
  "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

request
curl -s https://sealbind.io/api/v1/products \
  -H "Authorization: Bearer $SEALBIND_API_KEY"
200 response
{ "products": [ /* product objects in the shape above */ ] }

Get — GET /products/:id

request
curl -s https://sealbind.io/api/v1/products/clx0product0000000000000 \
  -H "Authorization: Bearer $SEALBIND_API_KEY"
200 response
{ "product": { /* product object */ } }

Update — PATCH /products/:id

Send any subset of the create fields. Set licenseDurationDays to null to clear it.

request
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}'
200 response
{ "product": { /* updated product object */ } }

Delete — DELETE /products/:id

request
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.

request
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"}}'
201 response
{
  "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

list request
curl -s https://sealbind.io/api/v1/licenses \
  -H "Authorization: Bearer $SEALBIND_API_KEY"
list response
{ "licenses": [ /* licence objects in the shape above, newest first */ ] }
get request
curl -s https://sealbind.io/api/v1/licenses/ABCD-EFGH-JKLM-NPQR \
  -H "Authorization: Bearer $SEALBIND_API_KEY"
get response
{ "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

request
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"}'
200 response
{
  "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

request
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"}'
200 response
{ "activation": { /* activation object with deactivatedAt set */ } }

Errors are 400 invalid_fingerprint or 404 license_not_found/activation_not_found.

Validate — POST /licenses/:key/validate

request
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"}'
200 valid response
{
  "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

request
curl -s -X POST https://sealbind.io/api/v1/licenses/ABCD-EFGH-JKLM-NPQR/revoke \
  -H "Authorization: Bearer $SEALBIND_API_KEY"
200 response
{ "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.