Licenses

A license is an activation-counted credential for a licenseEnabled product. Issue one per purchase; the buyer's software calls activate to bind a device and validate on each launch. Licenses pair with deliveries: the delivery hands over the bits, the license gates how many devices can run them. The Python SDK wraps six endpoints behind fulkruma.licenses. For HTTP shapes, see API → Licenses.

Namespace

fulkruma.licenses     # LicensesResources

The first three methods (issue, list, revoke) need the workspace HMAC key. The last three (activate, deactivate, validate) are public — called by the licensed software on the buyer's machine, which doesn't have your secret. The SDK still attaches the HMAC header when configured to (it doesn't hurt), but the backend treats these routes as unauthenticated.

Methods

issue

fulkruma.licenses.issue(body: dict, *, on_behalf_of: str | None = None) -> dict

Issues a license for a product + customer pair. Pass maxActivations to cap how many devices can use the key. Pass expiresAt for a time-limited license. The SDK auto-mints an idempotency key.

from datetime import datetime, timedelta, timezone

result = fulkruma.licenses.issue({
    "productId": "prod_01HX...",
    "customerId": "cus_01HX...",
    "maxActivations": 3,
    "expiresAt": (datetime.now(timezone.utc) + timedelta(days=365)).isoformat(),
    "externalSource": "storlaunch",
    "externalRef": "order-118-license",
})

print(result["license"]["key"])  # "FULK-XXXX-XXXX-XXXX-XXXX"

The key is generated server-side using a high-entropy alphabet and is the only thing the buyer's software ever sees.

list

fulkruma.licenses.list(*, on_behalf_of: str | None = None) -> dict

Returns every license in the workspace. Not paginated.

result = fulkruma.licenses.list()
active = [l for l in result["licenses"] if l["status"] == "active"]

revoke

fulkruma.licenses.revoke(license_id: str, *, on_behalf_of: str | None = None) -> dict

Marks a license as revoked. Subsequent validate calls return valid: False, status: "revoked". There's no "un-revoke" — issue a fresh license to restore access.

fulkruma.licenses.revoke("lic_01HX...")

Revoke is the right tool for chargebacks, fraud, and refunds.

activate (public)

fulkruma.licenses.activate(body: dict) -> dict

Called by the buyer's software on first launch to bind a device. Pass the license key and a stable instanceId. Returns alreadyActive: True if the (key, instanceId) pair is already registered — treat that as success, not error.

result = fulkruma.licenses.activate({
    "key": "FULK-XXXX-XXXX-XXXX-XXXX",
    "instanceId": "550e8400-e29b-41d4-a716-446655440000",
})
if result.get("activation"):
    pass  # success — cache locally

If the license already has maxActivations instances registered, the call returns 409 max_activations_exceeded.

deactivate (public)

fulkruma.licenses.deactivate(body: dict) -> dict

Releases an instance — the user is uninstalling, or moving to a new device.

fulkruma.licenses.deactivate({
    "key": "FULK-XXXX-XXXX-XXXX-XXXX",
    "instanceId": "550e8400-e29b-41d4-a716-446655440000",
})

validate (public)

fulkruma.licenses.validate(
    *,
    key: str,
    product_id: str | None = None,
) -> dict

Called by licensed software on every launch (or periodically). Returns the current license state without binding a new activation.

status = fulkruma.licenses.validate(key="FULK-XXXX-XXXX-XXXX-XXXX")
if not status["valid"]:
    raise SystemExit("License invalid")

Reasons a license can be invalid: revoked, expired, not_found, product_mismatch.

Types

Returned dicts use plain JSON shapes. Key entries:

# license
{
    "id": "lic_...",
    "accountId": "acc_...",
    "productId": "prod_...",
    "customerId": "cus_...",
    "key": "FULK-...",
    "status": "active" | "revoked" | "expired",
    "activations": int,
    "maxActivations": int | None,
    "expiresAt": "..." | None,
    "createdAt": "...",
    "updatedAt": "..."
}

# validate response
{
    "valid": bool,
    "key": "FULK-...",
    "status": "active" | "revoked" | "expired" | None,
    "productId": "prod_..." | None,
    "activations": int | None,
    "maxActivations": int | None,
    "expiresAt": "..." | None
}

Common patterns

Issue on purchase. Tied to a plugipay.checkout.completed webhook:

def on_checkout_completed(fulkruma, session: dict):
    pres = fulkruma.products.get(session["productId"])
    product = pres["product"]
    if not product.get("licenseEnabled"):
        return
    result = fulkruma.licenses.issue({
        "productId": product["id"],
        "customerId": session["customerId"],
        "externalRef": session["id"],
        "externalSource": "plugipay",
    })
    email_license(session["customerId"], result["license"]["key"])

Software-side validate-on-launch. Pseudo-Python for the buyer's app:

from fulkruma import FulkrumaClient

# No secret needed for validate/activate/deactivate. The constructor
# requires SOME credentials, so use the merchant's distributed public
# keyId and any string as secret.
fulkruma = FulkrumaClient(key_id="AKIAFULK_DIST", secret="unused-for-public-routes")

def startup(saved_key: str):
    status = fulkruma.licenses.validate(key=saved_key)
    if not status["valid"]:
        show_license_invalid_screen()
        return
    # re-validate every hour via your own scheduler

Deactivate-on-uninstall.

fulkruma.licenses.deactivate({"key": saved_key, "instanceId": saved_instance_id})

This frees a slot so the user can re-activate on a new machine without hitting max_activations_exceeded.

Bulk revoke for refund batch.

def revoke_for_refunds(fulkruma, license_ids: list[str]):
    for lid in license_ids:
        fulkruma.licenses.revoke(lid)

Errors

err.status err.code Cause
400 validation_error Missing required IDs, bad expiresAt.
404 not_found License ID or key missing or wrong workspace.
409 max_activations_exceeded Activate against an already-full license.
403 insufficient_scope Key lacks fulkruma:license:write (on issue/revoke).
400 product_not_licensed Issuing on a product where licenseEnabled = False.

Next