openapi: 3.1.0 info: title: smartEngin Licence & buy — Licensing API version: "1.0.0" description: > Developer-facing licensing endpoints of the smartEngin Licence & buy server (WordPress REST namespace `sels/v1`). These are the endpoints a licensed product uses to activate a site, re-validate a key, and receive updates. The shop's checkout, webhook, portal, account and invoice routes share the namespace but are internal and intentionally not documented here. Authentication model: these endpoints use **no authentication scheme** — no API-key header, no bearer token, no OAuth. The licence **key is the credential** and is sent as an ordinary request parameter (`key`); the server checks that the key exists and belongs to the given `product`. This is why the global `security` is an empty list. Transport & limits: HTTPS is required (except on `local`/`development` server environments). A per-IP + per-key sliding rate limit applies (default 30 requests/minute); exceeding it returns HTTP 429 with `error: rate_limited`. A licence key is a random, server-stored token — there is nothing to decrypt client-side. Enforcement (activation limits, the update channel, signed downloads) lives only on the server. Clients should gate features fail-open on the last known status. license: name: GPL-2.0-or-later (library) / example snippets are free to use url: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html servers: - url: https://smartengin.de/wp-json/sels/v1 description: > Current licence server. NOTE: the server may later move to a dedicated domain; keep this base URL in a single place in your integration. # No global security scheme: the licence key itself is the credential, passed as the # `key` parameter (see info.description). An empty list states this explicitly. security: [] tags: - name: licensing description: Activation, validation, and updates for licensed products. paths: /activate: post: tags: [licensing] summary: Register this instance against a licence key (idempotent). requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: [key, product, instance] properties: key: { type: string, example: "XXXX-XXXX-XXXX-XXXX-XXXX" } product: { type: string, example: "acme-gallery-pro" } instance: { type: string, example: "example.com", description: "Normalized host for domains: no scheme, no www, lower-case." } instance_type: { type: string, enum: [domain, device], default: domain } label: { type: string, description: "Optional human label." } responses: "200": description: Activated (or already active on this instance). content: application/json: schema: { $ref: "#/components/schemas/ActivationResult" } example: success: true status: active valid_until: "2027-01-31 23:59:59" activations_left: 2 "400": description: "`bad_request` — missing key or instance." content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } "403": description: "`license_inactive` (refunded/disabled), `trial_used_on_site` (a free trial was already used on this website), or `https_required`." content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } "404": description: "`product_not_found` (unknown slug) or `license_not_found` (key unknown or not for this product)." content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } "409": description: "`limit_reached` — the licence's activation limit is reached." content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } "429": { $ref: "#/components/responses/RateLimited" } /deactivate: post: tags: [licensing] summary: Free this instance's activation slot (idempotent, always succeeds). requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: [key, instance] properties: key: { type: string } instance: { type: string } instance_type: { type: string, enum: [domain, device], default: domain } responses: "200": description: Slot released (also returned when nothing matched). content: application/json: schema: type: object properties: success: { type: boolean, example: true } example: { success: true } "403": { $ref: "#/components/responses/HttpsRequired" } "429": { $ref: "#/components/responses/RateLimited" } /validate: post: tags: [licensing] summary: Re-check a key's current status. Never blocks (unknown key => valid:false). requestBody: required: true content: application/x-www-form-urlencoded: schema: type: object required: [key, product] properties: key: { type: string } product: { type: string } instance: { type: string } instance_type: { type: string, enum: [domain, device], default: domain } responses: "200": description: > Current status. Always HTTP 200 — even for an unknown key, which returns `valid:false` and `status:"unknown"` rather than an error. content: application/json: schema: { $ref: "#/components/schemas/ValidationResult" } examples: valid: summary: A valid, active licence value: { valid: true, status: active, valid_until: "2027-01-31 23:59:59", activations_left: 2 } unknown: summary: Unknown key (still HTTP 200) value: { valid: false, status: unknown } "403": { $ref: "#/components/responses/HttpsRequired" } "429": { $ref: "#/components/responses/RateLimited" } /update: get: tags: [licensing] summary: Update check. WordPress plugins (.zip) and non-WordPress products (Windows .exe/.msi, other) alike. Tolerant — returns no-update on any doubt. description: > Returns the latest version and a signed download URL for a valid licence. The product's **platform** shapes the payload: WordPress products carry the WordPress-only fields (`requires`, `requires_php`, `tested`); non-WordPress products omit those and instead carry `filename` (the real package file name to save the download as). Desktop apps send `instance_type=device`. parameters: - { name: key, in: query, required: true, schema: { type: string } } - { name: product, in: query, required: true, schema: { type: string } } - { name: version, in: query, required: true, schema: { type: string }, description: "Installed version." } - { name: instance, in: query, required: false, schema: { type: string } } - { name: instance_type, in: query, required: false, schema: { type: string, enum: [domain, device], default: domain }, description: "Use device for desktop/Windows apps." } responses: "200": description: > Either "no update" or an available update with a signed package URL. Any missing entitlement, unknown key/product, or "already current" yields the no-update form (still HTTP 200), so the client never breaks. content: application/json: schema: oneOf: - $ref: "#/components/schemas/NoUpdate" - $ref: "#/components/schemas/UpdateAvailable" examples: no_update: summary: No update available value: { success: true, update: false } update_available: summary: Update available (WordPress plugin) value: success: true update: true slug: acme-gallery-pro new_version: "1.1.0" package: "https://smartengin.de/wp-json/sels/v1/download?token=eyJ…" platform: wordpress requires: "6.4" requires_php: "7.4" tested: "6.4" changelog_url: "https://example.com/changelog" homepage_url: "https://example.com" sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" update_available_windows: summary: Update available (Windows app) value: success: true update: true slug: acme-desktop new_version: "1.1.0" package: "https://smartengin.de/wp-json/sels/v1/download?token=eyJ…" platform: windows filename: "acme-desktop-1.1.0-ab12cd.exe" changelog_url: "https://example.com/changelog" homepage_url: "https://example.com" sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" "403": { $ref: "#/components/responses/HttpsRequired" } "429": { $ref: "#/components/responses/RateLimited" } /download: get: tags: [licensing] summary: Stream the update package for a signed token from /update. description: > The `token` is produced by `/update`; you do not build it yourself. It is a signed (HMAC), URL-safe, short-lived token — treat it as opaque. On success the endpoint streams the package (WordPress .zip, Windows .exe/.msi, or any file product) and exits; on failure it returns a JSON error. The response's `Content-Disposition` header carries the real file name. parameters: - { name: token, in: query, required: true, schema: { type: string }, description: "Signed, short-lived token issued by /update. Opaque; expires quickly." } responses: "200": description: "The package file stream. Content type varies by file: application/zip, application/octet-stream for .exe/.msi, etc." content: application/octet-stream: schema: { type: string, format: binary } "400": description: "`bad_token` — missing or malformed token." content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } "403": description: "`token_expired` (link no longer valid), `license_inactive`, or `https_required`." content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } "404": description: "`license_not_found`, `no_package` (product has no package), or `file_missing`." content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } components: responses: RateLimited: description: "`rate_limited` — too many requests (per-IP + per-key, default 30/min)." content: application/json: schema: { $ref: "#/components/schemas/Error" } example: { success: false, error: rate_limited, message: "Too many requests. Please try again shortly." } HttpsRequired: description: "`https_required` — the request was made over plain HTTP in production." content: application/json: schema: { $ref: "#/components/schemas/Error" } example: { success: false, error: https_required, message: "HTTPS is required." } schemas: Error: type: object properties: success: { type: boolean, example: false } error: { type: string, description: "Machine-readable slug, e.g. license_not_found, limit_reached, rate_limited.", example: license_not_found } message: { type: string, description: "Human-readable message.", example: "Unknown licence for this product." } ActivationResult: type: object properties: success: { type: boolean, example: true } status: { type: string, enum: [active, expired, refunded, disabled, unknown] } valid_until: type: [string, "null"] description: "MySQL datetime, or null for a lifetime licence." activations_left: type: [integer, "null"] description: "Remaining slots, or null when unlimited." ValidationResult: type: object properties: valid: { type: boolean, description: "True only when active AND not past valid_until." } status: { type: string, enum: [active, expired, refunded, disabled, unknown] } valid_until: { type: [string, "null"] } activations_left: { type: [integer, "null"] } NoUpdate: type: object properties: success: { type: boolean, example: true } update: { type: boolean, example: false } UpdateAvailable: type: object properties: success: { type: boolean, example: true } update: { type: boolean, example: true } slug: { type: string } new_version: { type: string } package: { type: string, format: uri, description: "Signed, short-lived /download URL." } platform: { type: string, enum: [wordpress, windows, other], description: "Product platform; selects which fields below are present." } filename: { type: string, description: "Non-WordPress only: the real package file name (extension) to save the download as." } requires: { type: string, description: "WordPress only: minimum WordPress version." } requires_php: { type: string, description: "WordPress only: minimum PHP version." } tested: { type: string, description: "WordPress only: tested-up-to WordPress version." } changelog_url: { type: string, format: uri } homepage_url: { type: string, format: uri } sha256: { type: string, description: "Package checksum; present when computable. Verify it before installing." }