Page contents

API version 2026-09-21

API reference

Product app integration (Runtime API)

An API for product applications distributed to customers. Add device license activation, license-state checks, concurrency limits, and fully offline operation to your product.

Service overview

Usakey is not a service or technology that makes cracking impossible.

No defense is perfect against software analysis.
Usakey prevents accidental license violations, raises the cost of finding workarounds, records misuse when it occurs, and keeps the product able to stop access remotely.

  • Prevent accidental violations

    The product app enforces expiry, device-count, and concurrent-use limits so users do not unknowingly exceed the contract.

  • Raise the cost of workarounds

    Per-device keys and signed responses make key sharing and response forgery harder.

  • Record misuse

    Device activations, configuration changes, and over-limit use are recorded for review in Console.

  • Keep remote control available

    Suspension and revocation take effect at the product app's next periodic check.

If your Usakey plan ends or your workspace is suspended, protected features stop working in product apps you have already distributed, starting with their next periodic check. The Runtime API then returns 402 BILLING_REQUIRED. The purchaser's license is not suspended, so the product app should show “Temporarily unavailable. Contact the seller.” (see the product-side decision table).

What this page covers

What you will learn

  • How a product app authenticates a device and decides whether it may run
  • How to sign requests and what the API returns
  • Implementation choices for suspension, expiry, and network failure

Prerequisites

  • Register a product and be able to issue a license in Console
  • Calculate Ed25519 signatures and SHA-256 in the product app
  • Get the product ID and public-key list for signature verification from API/SDK

Shortest path

  1. Understand the complete model in How authentication works
  2. Prepare settings and keys in Prepare these values first
  3. Choose behavior for every state in the Product-side decision table

To have an AI assistant implement it for you, see Part 0 · Quickstart.

Part 0 · Quickstart

Get started with an AI assistant

Usakey provides an MCP server for AI assistants (https://usakey.jp/mcp). Sign up, add it to Claude Code or Codex, sign in to Usakey in your browser, and the AI can read Usakey skills (step-by-step guides for AI) to integrate licensing into your product and sell through Stripe. You can also ask it to issue or suspend licenses during everyday operations. If you prefer to understand the system and implement it yourself, start with Part 1.

  1. Sign up

    Create your Usakey account. If you already have one, go on to step 2.

    Create an account
  2. Add the MCP server

    Paste this instruction into an AI assistant that can run commands, such as Claude Code or Codex.

    Add https://usakey.jp/mcp to this AI assistant as Usakey's MCP server with the name usakey (HTTPS connection). After adding it, tell me how to sign in to Usakey in a browser and approve the connection. When the browser opens, I will sign in and approve the connection. If I have not registered with Usakey yet, I will register on that screen and come back to the approval screen.

    When Usakey opens in your browser, sign in and approve the connection. If you have not registered yet, you can register on that screen and come back to the approval. To add the server yourself with a command, see the detailed steps for adding MCP.

  3. Ask in plain language

    Describe the feature to protect. Following the skill, the AI brings in the official SDK, implements the check, and builds the suspended, expired, and offline states with tests. You can also ask it to issue or suspend licenses day to day (what you can ask).

    Use the Usakey skill to build a simple clock app in Rust with GPUI and protect it with a license.

Add MCP (details)

Usakey operates this MCP server. You do not need to prepare Node.js or an SDK, or create an API key. To have the AI add it, paste the instruction from step 2 above. After it is added, follow the AI's instructions to sign in in the browser and approve the connection. In Claude Code, choose usakey with /mcp and select “Authenticate”; Codex opens the browser right after the server is added.

Add it yourself

You can also add the server with a command instead of the instruction.

Claude Code
claude mcp add --transport http --scope user usakey https://usakey.jp/mcp

Open /mcp in Claude Code, choose usakey, and press “Authenticate” to open the browser. From a terminal, you can also run claude mcp login usakey. You are done when claude mcp get usakey shows ✔ Connected.

Codex
codex mcp add usakey --url https://usakey.jp/mcp

Adding the server starts sign-in and opens the browser. If it does not open, open the displayed URL or run codex mcp login usakey. You are done when Auth is OAuth in codex mcp list.

Other AI assistants
Any MCP client that supports Streamable HTTP and OAuth (browser sign-in) can add https://usakey.jp/mcp. Confirmed clients include Claude Code 2.1 and Codex CLI 0.152.

Sign in and approve in the browser

If you are not signed in, the Usakey sign-in page opens first (and asks for a verification code if you have two-factor authentication enabled). If you have not registered yet, register on that page and you come back to the approval. On the following consent screen, choose the workspace and the permissions to grant the assistant.

Use skills only
Read the integration instructions (skills) for the product app. No license management actions.
Allow read-only access
Also view products, policies, customers, licenses, audit events, and usage.
Allow day-to-day operations
Also create customers, issue, suspend, resume, and revoke licenses, and deactivate devices.
Allow product, policy, and webhook configuration
Also create and change products and policies, and add or change webhook destinations. A webhook destination sends license events to an external URL.

Only workspace owners and administrators can grant management access. When granting it, also choose one of these environments.

  • Test (test-environment products only)
  • Production (changes licenses for distributed products)
  • Both (test and production products)

To allow “Allow day-to-day operations” or more in production or both, the person granting it must have two-factor authentication set up. Usakey creates an API key dedicated to this connection and shows it under “API keys” with a name beginning MCP: (with both, one key per environment, so one connection shows two keys). Tools for user-level licensing are checked by default on plans that include them. Tools for sign-in settings (the customer portal's OIDC connections) start unchecked. The initial choice is “Allow read-only access” when Usakey could verify the app's publisher and your role can grant management access; otherwise it is “Use skills only”. Options your plan does not include are dimmed on the consent screen with one of these reasons.

  • This workspace's plan can't create production products, so Production and Both aren't available (Personal plan or higher).
  • This workspace's plan doesn't include user-level licenses (Personal plan or higher).
  • This workspace's plan doesn't include customer organizations or resellers (Team plan or higher).
  • This workspace's plan doesn't include OIDC sign-in for the customer portal (Team plan or higher).

For which environment's key the assistant uses on a connection with both (the environment argument, monthly calls for lists, and more), see the External integration (Management API) guide.

Tools for operations you did not allow are not shown to the assistant. When it connects, the assistant is told which features your plan does not include, limits such as the number of products and the allowed policy values, and this month's usage. It can also check them with get_plan_limits, which does not use the monthly allowance. The assistant is instructed to stop before calling a tool when a request goes beyond these limits and to offer you choices.

You can revoke the connection at any time from AI assistant connections in Console. After revocation, or after the sign-in lifetime expires (30 days after last use), sign in again from the AI assistant.

What MCP can do

Every connection can read the skills. Management tools are shown only for the permissions approved on the consent screen.

CapabilityExample requestRequired permission
Read skills and integrate them (device authentication, Stripe sales)“Build a simple clock app in Rust with GPUI and protect it with a license”Use skills only (all connections)
View products, policies, customers, licenses, and device activations“List expired licenses”Allow read-only access
Check the integration settings (usakey.config.json), plan limits and this month's usage, device check-in status, and your Stripe integration“Save this product's integration settings as usakey.config.json”Allow read-only access
Review audit logs and usage“Tell me the number of new device activations in the last 30 days and today's change history”Allow read-only access
Register customers and issue licenses“Register Sample Co. and issue a license with the standard policy”Allow day-to-day operations
Suspend, resume, or revoke licenses; deactivate activations“Suspend lic_… because of a payment delay”Allow day-to-day operations
Create or change products and policies; add or change webhook destinations“Create a product and a monthly policy for up to three devices”Allow product, policy, and webhook configuration
Sealed files (Team and above): turn them on and get the sealing key, seal the files you ship, and build opening them into the product app“Turn on sealed files for this product, seal models/pro.onnx, and make the app open it”Sealing and the app code need only the skill (all connections). Checking needs “Allow read-only access”; turning it on needs “Allow product, policy, and webhook configuration” (otherwise turn it on in the Console)
User-level licensing (resellers, customer organizations, groups, license users, assignments)“Add Tanaka to the Sales group”“Also allow tools for user-level licenses (resellers, organizations, groups, and license users)” on the consent screen (checked by default where the plan includes it; Personal and above, customer organizations and resellers on Team and above)
View, change, or disable sign-in settings for the customer portal (OIDC connections)“Show the customer portal's OIDC connections”Select the sign-in settings tools on the consent screen (unchecked by default); changes need “Allow product, policy, and webhook configuration” (Team and above)

What MCP does not do

  • Delete products, or create OIDC connections and change their client secrets (these need IdP secrets, so use Console).
  • Issue offline activation files or save license keys to local files (use Console or the local API-key server).

Each management-tool call makes one Management API call. It uses one call of your plan's monthly allowance (get_plan_limits and calls refused without changing anything, such as invalid input, do not count; neither does reading skills). A license key is included in the issuance response and remains in the AI conversation. To keep it out, ask the assistant to set reveal_key: false when issuing; the key is left out of the response and can be shown on the license page in Console. Review every change in the assistant's confirmation screen before approving it.

Usakey's MCP server supports the latest MCP protocol (2026-07-28: stateless requests include the version and client capabilities) and the initialization-based protocols from 2025-03-26 through 2025-11-25. Skills are available both through the MCP skill extension (io.modelcontextprotocol/skills) and ordinary resources (skill://…). See the MCP section of External integration (Management API) for details.

What the skills provide (for AI developers)

The skills are AI-oriented implementation guides covering Parts 1 and 2 of this page. The product-integration and Stripe-sales skills are distributed together. If MCP is connected, the assistant reads them through MCP, so no separate installation is needed. After reading a skill, the AI maps the protected features in your existing app and implements and tests them against Usakey's current protocol.

Integrate into a product app

usakey-auth-integration

  • Fetch the official SDK and integrate it for the target language.
  • Choose node-locked, floating, or fully offline licensing.
  • Generate and securely store device keys.
  • Schedule checks with enough allowance for every plan, including startup and retries.
  • Implement and test every success and failure state, including UI and feature gating.
  • Accept and store the license key entered by the user.
  • Sealed files (Team and above): seal the files you ship and build opening them into the product app, with tests.
  • Handle network failure, explicit denial, and invalid signatures separately.

Sell through Stripe

usakey-stripe-integration

  • Create purchase claims and pass them to Payment Links or Checkout Sessions.
  • Receive and securely store the license key after payment, then acknowledge receipt.
  • Handle failed payments, delayed settlement, retries, cancellation, full and partial refunds, and trial conversion.
  • Resume claiming after a lost response or an app restart.
  • Test every path end to end in Stripe's test environment.

You receive the payment in your own Stripe account and issue, suspend, or revoke licenses to match payment (license issuance tied to purchases). Register your Stripe integration in Console first (a paid plan is required).

The skill installs the SDK

When you ask the AI to integrate Usakey, the official SDK is fetched and verified by the bundled scripts/fetch-sdk.sh, then placed in vendor/usakey-sdk/ in the product repository. You do not need to install the SDK separately. When the skill is read through MCP, the assistant downloads the skills ZIP (usakey-skills.zip) and its checksum to a temporary folder outside the product repository, verifies them, and extracts the ZIP only when they match. It shows you the fetch script and the command it will run, asks for approval before running it, and deletes the temporary folder afterward. Review what it shows and approve it. Where the ZIP cannot be downloaded, the assistant saves the files from the MCP resources and checks them against the SHA-256 that MCP provides before using them.

The destination must be one folder inside vendor/ directly under the product repository. The script does not replace the repository root, a home folder, a path outside the repository, or a symbolic link. The current skill version is 2026.10.11.3, and the SDK version it installs is 2026.10.11.3. If the vendored SDK is 2026.09.24.1 or later, you don't need to fetch it again when the skill is updated. The skill's VERSION records the SDK checksum and refuses to extract a mismatch.

Skills support design and implementation, but do not automatically guarantee production safety. Use the product ID, API URL, trust anchor, and protected feature names for the target environment. Never reuse demo license keys or fixed private keys in production.

Install skills only (optional)

If you do not use MCP, or your AI does not support MCP, place the ZIP containing both skills where the AI can load it. The ZIP contains usakey-auth-integration and usakey-stripe-integration directly under its root.

Place the skills

Paste this instruction into your AI assistant. It will download and place the skills where they can be used.

Make the skills in https://usakey.jp/downloads/usakey-skills.zip available to this AI assistant

Use this with an AI that can fetch or create files. If your AI only supports conversation, use the instructions.

For API-key operation (such as CI where a browser cannot open), or when you want the AI to issue offline activation files and save keys to files, you can also use the local MCP server with an API key included with the official SDK.

Part 1 · Authentication model

How authentication works

Usakey authentication checks whether a valid contract exists and whether the request came from a registered device, then returns the result to the product app. The user does not sign in every time the app runs.

Administrator

Issue a license

Set the product, term, device limit, and available features, then give the license key to the user.

User and product app

Enter the key and create device keys

The product app generates two keys on the device and sends only the public keys to Usakey.

Usakey

Verify the contract and device

Check the key, product, device limit, and whether the device signature is valid.

Product app

Verify the signed entitlement

Enable licensed features only after confirming that the response is authentic.

Figure 1: On first launch, the license key requests access and binds the contract to the device. Private keys never leave the device.

Keep three things in mind

License key · device keys · Usakey signature

The license key requests access, and the device-only private key proves that the request came from a registered device. Usakey signs the decision, and the product app enables features only after checking that signature.

License key
A string issued by an administrator and given to the user. It is used only to register the device for the first time.
Device keys
A key pair generated by the product app on the device. The private key stays on the device; only the public key is sent.
Usakey signature
A signature on responses and certificates that detects forgery and changes made in transit.

Actors and responsibilities

ActorResponsibilityHolds
AdministratorSets contract terms and issues, suspends, resumes, or revokes licenses.Console account
UserEnters the license key received from the administrator when launching the product for the first time.License key
Product appGenerates device keys, checks license state, and enables or disables protected features.Product ID, device private keys, verified certificates
UsakeyEvaluates the contract, device count, and concurrent use, then returns a signed result.License data and registered device public keys

Product signing keys and plan differences

Usakey signs responses and entitlements with keys separated per product. It prepares signing keys when they are actually needed, such as for the first license issuance. You can configure a product ID and policy before its signing key is ready.

Evaluation plan
A shared issuing key authorizes a product-specific signing key for a limited purpose and term. The product private key is generated when needed, encrypted at rest, never reused for another product, and automatically renewed as expiry approaches.
Paid plans
A non-extractable key dedicated to the product is prepared when needed for both test and production products. The private key is never exported to the Usakey application.

Moving from Evaluation to a paid plan does not require a product-app change. Usakey keeps the existing product ID, API URL, initial trust anchor, and Trust Bundle sequence, and prepares the product key before the next use.

PRODUCT_SIGNING_KEY_PENDING is retryable. It may occur on first use, after an Evaluation signing key expires, or just after changing plans. A Management API license issuance that returns HTTP 409 should wait a few minutes and retry with the same request body and Idempotency-Key. Offline-file issuance does not use an idempotency key; retry with the same request file and options. Runtime API HTTP 503 follows Retry-After and should be treated as a temporary SDK failure. Do not recreate the product or change SDK settings.

First launch

The first launch binds a license to its device. This page calls that registration “device activation”; API URLs and field names use activation. The device key is checked at the same time so that another device that only knows the license key cannot impersonate a registered device.

  1. Administrator → user

    Give the license key

    Send the USK-… issued in Console to the user through a secure channel.

  2. Product app (on device)

    Generate two key pairs

    Generate a signing key and a data-receiving key. Neither private key leaves the device.

  3. Product app → Usakey

    Request registration

    Send the license key, product ID, two public keys, and a one-time nonce, with a device signature.

  4. Usakey

    Verify the contract and device

    Check the key, product, device signature, and remaining device capacity.

  5. Usakey → product app

    Return a signed entitlement

    Return the activation ID and a certificate containing state, expiry, and usage terms.

  6. Product app

    Verify before enabling features

    Verify response and certificate signatures, product ID, and device binding. Never make a decision from unverified data.

The license key is sent only during first registration. Later requests use the activation ID and a signature made with the device private key. The private key is never sent to the server.

Periodic checks while running

At the interval specified by Usakey, the product app checks whether this device may continue to run. This page calls the request a “periodic check”; the API field name is heartbeat.

Product app

Sign a periodic check

Sign the activation ID, current time, one-time nonce, and request body with the device key.

Usakey

Re-evaluate current terms

Evaluate suspension, revocation, expiry, enabled features, and the next check time.

Product app

Verify and apply the result

Continue when allowed; otherwise show the reason and disable only the protected features.

Figure 2: A periodic check never resends the license key. The device private key proves that the request came from the registered device.

The response includes current state, the latest certificate, and seconds until the next check. A concurrency license also obtains and renews a short-lived lease for the current seat. See Data returned by the API for the field list.

Do not poll every second. Wait for next_heartbeat_in or heartbeat_in. The protocol minimum is 10 seconds, but each plan requires a longer minimum interval and limits the number of checks per day (see the table below). After HTTP 429, wait for Retry-After seconds and generate a new nonce and signature.

PlanMinimum device-state intervalScheduled checks (the per-day count on the pricing page)Hard limit before 429 (includes retries)
Evaluation 15 minutes 96 per day 4/min · 16/hour · 384/day
Personal 6 hours 4 per day 4/min · 8/hour · 16/day
Team 3 hours 8 per day 4/min · 12/hour · 32/day
Enterprise 1 hour 24 per day 4/min · 24/hour · 96/day

The hard limit allows 4 times the scheduled count so failed checks can be retried. Design the product app for the scheduled count shown on the pricing page.

Floating seats are available only on the Enterprise plan. Seat renewal is at least 30 minutes apart, with 4 renewals/minute, 12/hour, and 96/day per seat. Acquisition and release allow 6 operations/minute, 20/hour, and 100/day per seat.

A CLI that exits after every command reaches these limits quickly. Because a verified certificate lives only in the process, a one-command/one-process design performs a periodic check on every invocation. Use an interactive shell or resident process to retain verified state after one check, and call only the SDK decision method immediately before each operation. If 429 occurs, prioritize Retry-After, wait with a cap, and retry.

When the network is unavailable

A failed request alone does not mean that a license was suspended. The product app uses the last certificate it verified and the network-failure policy configured by the administrator to make a temporary decision.

Periodic-check result

Did the server return a clear response?

Denied response

Disable the feature

Suspension, revocation, expiry, and invalid signatures are not overridden by network grace.

Cannot connect

Continue only within policy

Follow the network-failure policy only while the last verified certificate remains valid.

Allowed response

Continue use

Store the new certificate and seconds until the next check.

Figure 3: Distinguishing “cannot connect” from “use is not allowed” prevents a temporary outage for a legitimate customer from being confused with a contractual suspension.
ResultProduct-app behavior
Signed suspension, or a revocation or expiry rejection (403 LICENSE_REVOKED / LICENSE_EXPIRED)This is not a network failure. Disable the feature and show the reason.
Invalid signature, product mismatch, or device mismatchDo not apply network grace. Disable the feature and record a security error.
Cannot connect or temporary server errorContinue while the last certificate is valid, then follow the configured stop, limited continuation, or warning behavior.

The contract expiry and lease expiry take precedence over network grace. See the Product-side decision table for exact branches.

After an app restart. The official SDK does not save verified certificates by itself. After a periodic check, export the verified state (export_verified_state, exportVerifiedState in Node.js, usakey_client_export_verified_state in the C ABI) and save it in a store that cannot be rolled back (such as StateStore below); after a restart, import it first on a new client (import_verified_state). Each import verifies the signatures again, and while the network is unavailable the decision uses the last certificate's expiry and the network grace (it is treated as offline until a periodic check succeeds). A state exported after revocation or deactivation, or a clock moved back by five minutes or more, cannot be imported. Without a saved state, restore the activation using its saved ID and keep protected features disabled until a new periodic response is verified. If the API explicitly denies use or the signature is invalid, persist that state and never fall back to the previous approval. Fully offline devices use the signed entitlement file described below.

Always-offline devices

Devices that cannot reach Usakey, such as devices on a closed network, do not perform online periodic checks. Signed files move from the target device to the administrator and back to the target device.

  1. 01 / Target device

    Create an activation request .usakeyreq

    It contains public keys and a device signature, never the license key or private keys.

  2. 02 / Administrator

    Issue .usakeylic in Console

    Bind the request device to the license and create a signed entitlement with an expiry.

  3. 03 / Target device

    Import .usakeylic

    Verify the signature, product, device, and expiry, then enable protected features only within the term.

Administrator changes cannot take effect immediately offline. If suspension and other changes must take effect sooner, issue files with a short expiry and replace the .usakeylic regularly. See Fully offline devices for the file and command details.

Detecting copies and tampering

CheckHow it is checkedRisk addressed
Did it come from a registered device?Verify the device-private-key signature with the registered public keyA request from another device that copied only the license key
Is this a replay?Verify time and the one-time nonce generated for each requestResending a previously successful request
Is the response from Usakey?Verify response and certificate signatures from the trust anchor shipped with the productA fake server granting unauthorized use
Was the response body changed?Compare the body hash, product ID, request nonce, and HTTP status with the signed assertionChanging state, expiry, or terms in transit or at rest
Was a certificate copied to another device?Check the product, device key, and activation binding in the certificateUsing a signed entitlement on another product or device

A signature cannot automatically detect theft of the device together with its private key. In production, store the device private key in the OS credential store or TPM. See Response verification order for the signed inputs and order.

When administrator changes take effect

Administrator actionOnline deviceFully offline device
Suspend, resume, or revokeThe next successful periodic check returns the new state and the app updates its UI and features.Not automatic. It takes effect when the current .usakeylic expires or a replacement file is imported.
Contract expiryThe certificate contains the expiry, so the license never remains valid beyond it even without a connection.Use the expiry inside .usakeylic.
Force-deactivate a deviceThe next signed request is rejected as deactivated and the app asks the user to activate again.Not automatic. Keep issued files short-lived to limit the impact period.
Change available featuresThe next periodic check returns a new certificate and the app re-evaluates the terms.Issue and import a new .usakeylic.

Design the product app to re-evaluate state while a screen remains open, rather than checking only once at launch. If use becomes unavailable, keep settings, status, and reactivation paths available and stop only the protected features instead of force-quitting the entire app.

That completes the model

For implementation, continue with Prepare these values first → Data returned by the API → Product-side decision table.

Part 2 · Implementation reference

Use the official SDK

Before implementing the protocol yourself, check whether the official SDK supports your language. It implements signature verification, canonicalization, public-key-list verification, entitlement verification, and floating-seat management. Do not implement cryptography yourself.

Supported languages

The core is written in Rust. C / C++, Python, Ruby, Node.js / TypeScript, Go, Java / Kotlin / Scala, C# / .NET, PHP, Flutter / Dart, Swift, and React Native call the same core through the . iOS and Android s call the same core through .

The tested combinations and unsupported combinations are listed in README.md and docs/support-matrix.md inside the ZIP.

Rust

Core implementation; other languages call this module

rust/usakey/

C

Example and Makefile using the public header (ffi/usakey_ffi/include/usakey.h)

c/

C++

Header-only RAII wrapper

cpp/

Python

ctypes

python/

Ruby

fiddle

ruby/usakey_runtime/

Node.js / TypeScript

koffi

node/

Go

cgo

go/

Java

JNA

java/

Kotlin

Uses the Java binding (Gradle setup and example)

kotlin/

Scala

Thin wrapper around the Java version

scala/

C# / .NET

P/Invoke

dotnet/

PHP

FFI extension

php/

Flutter / Dart

dart:ffi

flutter/usakey/

Swift (desktop)

modulemap + C ABI

swift/

React Native

JSI

react_native/

iOS / Android native

UniFFI (separate family)

mobile/

Install and use it

AI developer skills already install the SDK. The skill fetches it into vendor/usakey-sdk/, so the following manual steps are unnecessary.

  1. Download the ZIP and checksum, verify them, and extract the SDK into vendor/usakey-sdk/ in the product repository.
  2. For a C ABI binding, build the C ABI library in ffi/usakey_ffi. Rust and iOS / Android UniFFI bindings do not need this step.
  3. Read the README in the directory for your language.

The following is a Linux / macOS example. On Windows, compare the sidecar's first value with PowerShell Get-FileHash -Algorithm SHA256, extract with Expand-Archive, and follow the language README.

curl -fSLO https://usakey.jp/downloads/usakey-sdk.zip
curl -fSLO https://usakey.jp/downloads/usakey-sdk.zip.sha256
if command -v sha256sum >/dev/null 2>&1; then
  sha256sum -c usakey-sdk.zip.sha256
else
  shasum -a 256 -c usakey-sdk.zip.sha256
fi
unzip usakey-sdk.zip -d vendor/
cd vendor/usakey-sdk/ffi/usakey_ffi
cargo build --locked --release
case "$(uname -s)" in
  Darwin) export USAKEY_LIBRARY_PATH=$PWD/target/release/libusakey_ffi.dylib ;;
  Linux) export USAKEY_LIBRARY_PATH=$PWD/target/release/libusakey_ffi.so ;;
esac

USAKEY_LIBRARY_PATH is read by Python, Ruby, Node.js, Java / Kotlin / Scala, C#, PHP, and Flutter. Go (CGO_LDFLAGS; it looks in target/debug unless you set it), Swift (-Xlinker -L), C / C++ (USAKEY_PROFILE=release for the Makefile), and React Native (android/build-ffi.sh) set the library location at build time. On Windows, run cargo build --locked --release in the same directory and use target\\release\\usakey_ffi.dll (see README.md in the ZIP). Building and running the C ABI library on macOS and Windows has not been verified yet.

Keep the SDK under vendor/usakey-sdk/. The AI developer skills expect the same location and fetch and verify it in one step.

Commit the SDK to the product repository to pin its version, but exclude build output such as vendor/usakey-sdk/**/target/. The library and language code must come from the same release. The SDK detects layout mismatches at startup but cannot guarantee version equality.

The current SDK version is 2026.10.11.3. After downloading, check usakey-sdk/VERSION and the /downloads/usakey-sdk.zip.sha256 checksum.

If the integration does not work, the diagnostic in the bundled Rust core (usakey-doctor) checks the connection settings file (usakey.config.json), signing-key readiness, the public-key list, and clock skew without using a single periodic check (cargo run --locked --example usakey-doctor -- --config usakey.config.json; see README.md in the ZIP).

Node.js and Python come with integration starters (starters/). They are not part of the SDK itself but a base to copy into your product source: activation, heartbeat scheduling and a daily budget, state and device-key storage, user-facing messages (Japanese and English), a resident agent (Linux / macOS), and tests that exercise every outcome with a fake client that replaces the SDK. They have not been checked against a real server yet. See starters/README.md for usage and limits.

The functions that open files the seller sealed and ships with the product (sealed files) are in the Rust, C ABI, Node.js, Python, and Ruby SDKs (Receive the key for a sealed file). The tool that seals files (usakey-seal) also comes with the Rust core (cargo run --locked --example usakey-seal -- seal --key "$USAKEY_SEALING_KEY" FILE). Other bindings cannot open sealed files yet.

Rules to follow

  • SDK summary information is for display. Call the decision method for every protected feature.
  • Never pass the device private key to the SDK; provide an implementation that returns signatures only (DeviceIdentity works as is in the C ABI, Node.js, Python, and Ruby).
  • Do not let exceptions escape from signature callbacks. Keep the decision inside the SDK boundary.
  • After restart, restore using the saved identifier. Re-registering consumes another device slot.

The complete common rules are in README.md inside the ZIP.

Download the SDK

The tested range differs by operating system and language. Check README.md in the ZIP for the supported range. Combinations not listed are not covered.

Using Rust directly

  • Rust 1.82 or later is required by the crate's rust-version. Older toolchains cannot resolve or build it. If dependency resolution requires edition2024, use a Cargo version newer than the MSRV, such as 1.89.
  • The Cargo package is usakey-sdk and the crate is usakey. Add it to Cargo.toml as follows.
[dependencies]
usakey = { package = "usakey-sdk", path = "vendor/usakey-sdk/rust/usakey", features = ["blocking-session"] }
  • The blocking-session feature is required for usakey::session and its synchronous APIs such as BlockingSession, MachineInfo, and take_runtime_error_code. Omit it when using only async APIs. See rust/usakey/Cargo.toml [features] for valid feature names.

Verify encrypted storage and rollback detection

AES-256-GCM protects stored contents from reading and tampering, but by itself cannot detect replacing a file with an older correctly encrypted file. Compare the state generation with a value stored somewhere that cannot be rolled back with the state file: TPM, OS secure storage, or the latest generation recorded on a server.

Direct Rust SDK

Use EncryptedStateStore::with_rollback_guard and StateRollbackGuard. Save generation 1, save generation 2, then restore only the file to generation 1 and confirm StateStoreError::RollbackDetected. Change one byte near the end of the current file (the ciphertext) and confirm StateStoreError::Authentication (changing a header byte yields StateStoreError::InvalidEnvelope); protected features must remain disabled in both cases.

C ABI, Node.js, Python, and Ruby

Use StateStore (encrypted with AES-256-GCM, with the maximum generation kept where it cannot be rolled back; it rejects older versions, tampering, and files for another key or purpose), Keystore (the OS credential store, or fromProtector to call your own key protection), and DeviceIdentity (generates the device key once, keeps it in a dedicated store, and signs with it). In the C ABI these are usakey_state_store_open, usakey_keystore_open, usakey_device_identity_open, and related functions. Save the verified state (see “After an app restart” above) and purchase claims here too.

Other bindings (Go, Java / Kotlin / Scala, C#, PHP, Flutter, Swift, React Native) and iOS / Android native bindings

These s do not expose a rollback-detecting store. Store the activation ID (activation_id), clock snapshot (clock_snapshot), maximum sequence received, known revocation/deactivation/unknown-result states, and app generation in a per-product/per-device store that detects tampering and rollback. If a missing value, old generation, or tampering is found outside first launch, disable protected features before restoring the SDK state () and require an online check.

  1. For Rust, roll only the state file back while retaining the rollback record and confirm encrypted state rollback detected.
  2. For each binding, save generation 1 and 2, then reproduce a missing record, a replacement with generation 1, and a one-byte tamper independently. Each must be rejected before activation is restored.
  3. Store clock_snapshot and the maximum Trust Bundle sequence under the same policy. In an isolated test clock, move time back more than five minutes and confirm protected features stay disabled until an online check. Never change a production device clock.

If the state file and rollback record can be restored together, rollback detection across restart has not been proven. Being able to pass clock_snapshot or a minimum sequence to the SDK does not itself prove that the storage is protected.

Part 2 · Implementation reference

Online authentication protocol

If the official SDK supports your language, use this section to confirm the behavior the SDK provides rather than as a manual implementation recipe. On first launch, send the license key and device public keys to register the device. Afterwards, use the device signature to check license state and floating seats periodically. The OpenAPI and protocol call this family of endpoints the Runtime API.

Authenticate from Godot and other browser apps

Use this setting when CORS restrictions prevent a product app running in a browser, such as a Godot or WASM app, from connecting directly to Usakey. Register the app's origins and enable access to activate devices and check licenses directly from the browser.

Enable browser license authentication in the product form and register its origins. You can also read and change the settings with the MCP tools get_product_browser_runtime / update_product_browser_runtime, or GET / PATCH /mgmt/v1/products/{product_id}/browser-runtime (products:read / products:write). Access is disabled by default. Register up to 20 HTTPS origins; test products also accept local HTTP origins.

Supported operations include activation, periodic checks, deactivation, floating leases, email verification, trial signup, time checks, and public signature-verification information. Management API, Console, Portal, and sealed file key delivery are excluded. Device request signatures and response signature verification remain required.

Use credentials: "omit"with fetch. Cookie authentication is not used. Godot HTTP calls and WASM clients must use the existing signature protocol and request format. CORS support does not make native SDKs compatible with WASM.

OPTIONSpreflights do not consume device registrations, floating seats, nonces, monthly API usage, or outgoing email. A separate per-IP limit prevents abusive repetition.

An actual request from an origin that is not allowed for its target product is rejected with BROWSER_ORIGIN_NOT_ALLOWED. A refused preflight appears as a browser network error. Another product's settings or a cached preflight cannot grant access to the target product.

For threaded Godot web exports, configure COOP and COEP on the game hosting side. Adding these headers to the authentication API does not grant CORS access.

Prepare these values first

You can download these values as one file. In Console, open “Development & integrations” → API/SDK and download the product's connection settings. The usakey.config.json contains the API URL, product ID, initial trust anchor, and entitlement name. It has no secrets, so it can be stored in the product repository. AI developer skills read this file directly.

License key
The USK-... shown once immediately after an administrator issues a license. Give it to the customer; receive it from user input or an installer. Never embed a shared key in the binary.
Product ID
The prod_... shown under “Development & integrations” → API/SDK. It is not secret and can ship with the product app.
Device signing key
Ed25519
Proves that the request came from a registered device. Generate the pair once on the target device and store the private key in the OS credential store or TPM. Send only the public key to the API.
Device receiving key
KEM key
Delivers data in a form only that device can open. The signing key answers “who sent it?” while the receiving key answers “who can open it?”. Register the public key for device binding and future encrypted delivery; never export its private key.

The license key and device public keys are generated in different places. The administrator issues the license key in Usakey; the product app generates device keys on the device where it runs.

Development-server settings may omit the initial trust anchor. A development server that does not manage signing keys in a cloud key management service signs responses itself (signing_mode=development_self_signed). Its public key is development-only and is intentionally absent from trust_anchor.root_public_key. If you need to test signatures during development, obtain the key from that server's administrator. Always download production settings from the production Console before shipping.

Signature on first activation

Canonicalize the request body without device_proof as JSON Canonicalization Scheme (JCS), which produces the same ordering for every implementation. Prefix it with Usakey-Activation-Proof-v1\n and sign with the device signing private key (confirming a trial sign-up from the app uses the prefix Usakey-Trial-Proof-v1\n instead). Encode the signature as Base64URL for HTTP.

POST /runtime/v1/activations
Content-Type: application/json
Idempotency-Key: <UUID per request>

{
  "license_key": "USK-…",
  "product_id": "prod_…",
  "device_signing_public_key": "ed25519:…",
  "device_kem_public_key": "p256:…",
  "device_proof": "…",
  "machine_info": {"os": "windows", "arch": "x86_64"},
  "nonce": "…"
}

The one-time nonce (API field: nonce) is at least 16 cryptographic random bytes encoded as unpadded Base64URL and generated for every new logical request. It is different from the idempotency key (Idempotency-Key). Only an SDK retry after an unknown result reuses the same raw byte body and key. Confirming a trial sign-up from the app requires Idempotency-Key; retry with the same key and the same body. Save the activation ID (activation_id), verified certificate, and both private keys securely after the first response. The API never returns a private key.

Device signatures after registration

For periodic checks and floating-seat acquire, renew, and release, join these six lines with LF (the \n line ending) and sign with the device signing private key. Sort and form-encode query parameters; do not change even one byte of the body after signing.

Usakey-Request-v1
<UPPERCASE METHOD>
<path?sorted=query>
<Unix timestamp seconds>
<new nonce of at least 16 bytes>
<lowercase hex SHA-256 of raw body>

Worked signature input

This example sends the following one-line JSON body as UTF-8 without spaces or a trailing newline.

{"app_version":"2.0.0","machine_info":{"arch":"x86_64","os":"windows"},"sdk_version":"usakey-rust/0.1.0"}

Its SHA-256 is a63964d27ca2a35ccaca1ad52f18a513ae1d2f806011f93eeeae0125cd27a0c3. The six signature lines are:

Usakey-Request-v1
POST
/runtime/v1/activations/act_0123456789abcdef0123/heartbeat
1787621025
AAECAwQFBgcICQoLDA0ODw
a63964d27ca2a35ccaca1ad52f18a513ae1d2f806011f93eeeae0125cd27a0c3
Time
1787621025 = 2026-08-25 01:23:45 UTC
One-time nonce
AAECAwQFBgcICQoLDA0ODw is a fixed 16-byte example. Generate fresh cryptographic random bytes in production.
Final line
There is no newline after line 6.

First activation requires the defined machine_info fields. Send the minimum available OS and CPU architecture plus permitted OS, CPU, memory, storage, runtime, and security signals. Send the full object when a value changes or every 24 hours; an unchanged periodic check may send only the previous object's SHA-256. Never send hostname, machine UUID, machine-id, serial, MAC address, user name, home path, environment variables, file/app/process lists, or secrets. Compare the source IP and User-Agent observed by the server with the previous check and record changes in Console and the audit history. Reverse DNS is informational and is not identity proof.

Do not override fields that the SDK collects from the OS. For example, model is collected from the BIOS product name on Windows, /sys/class/dmi/id/product_name on Linux, and hw.model on macOS. Setting Rust MachineInfo.model is rejected as an override of a collected field (WSL or containers without DMI may hide this during development). Pass the app version with the dedicated app_version option. Fields that are not collected, such as app, can be set only through Rust ActivationOptions.machine_info / HeartbeatOptions.machine_info; session::MachineInfo, the C ABI, and the language bindings have no such field.

Set the activation ID, timestamp, nonce, and signature in X-Usakey-Activation, X-Usakey-Timestamp, X-Usakey-Nonce, and X-Usakey-Signature. Clock tolerance is ±5 minutes. Record used nonces for at least 10 minutes across the tenant to reject replays. For CLOCK_SKEW, verify signed server time from POST /runtime/v1/time and repair the trusted local clock baseline.

Data returned by the API

Successful activation, periodic-check, floating-seat, time, email-verification, and trial sign-up responses contain four elements: the decision (data), matching information (response_assertion), the short-lived-key delegation (signing_delegation), and the Usakey signature (response_signature). Do not use the decision until body hash, nonce, HTTP status, product ID, delegation, and signatures are verified. Errors use the error shape described below.

Operationdata fieldsSave in the product app
First activation
201
activation_id, certificate, machine_info_digestActivation ID, verified certificate, and hash of the submitted machine information
Periodic license check
200
activation_id, license_status, certificate, next_heartbeat_in, machine_info_digestNew certificate, seconds until next check, and the server-side machine-information hash
Acquire floating seat
201
lease_id, instance_id, expires_at, heartbeat_inLease ID and expiry
Renew floating seat
200
lease_id, instance_id, expires_at, heartbeat_inUpdated expiry
Release floating seat
200
lease_id, status="released"Discard the local lease
Start email verification
202
verification_id, expires_at, resend_after_sec, code_lengthThe verification ID (used to send the code). Use the resend seconds for the screen
Email verification
200
named_user_token, expires_atDo not store it; pass it to activation as named_user_token right away
Start a trial from the app
202
verification_id, expires_at, resend_after_sec, code_lengthThe verification ID (used to send the code)
Trial confirmation (issue and activate)
201
activation_id, certificate, machine_info_digest, license_idSame as first activation. No license key is returned
Server time
200
server_time, sdk_versionVerified time as the trusted basis for local clock correction
{
  "data": {
    "activation_id": "act_...",
    "license_status": "active",
    "certificate": "v4.public....",
    "next_heartbeat_in": 86400,
    "machine_info_digest": "<SHA-256 of machine_info>"
  },
  "response_assertion": {
    "v": 1,
    "product_id": "prod_...",
    "key_id": "key_online_...",
    "parent_key_id": "key_prod_...",
    "request_id": "...",
    "nonce": "...",
    "server_time": 1787623200,
    "status": 200,
    "body_sha256": "<SHA-256 of JCS(data)>"
  },
  "response_signature": "...",
  "signing_delegation": {
    "delegation": {
      "v": 1,
      "purpose": "runtime-response",
      "product_id": "prod_...",
      "parent_key_id": "key_prod_...",
      "online_key_id": "key_online_...",
      "public_key": "-----BEGIN PUBLIC KEY-----\n...",
      "issued_at": 1787620000,
      "expires_at": 1787663200
    },
    "signature": "..."
  }
}

certificate is a signed PASETO v4.public entitlement. It is readable plaintext; the signature detects tampering, but it is not encrypted. Verify its product ID, activation ID, device-key hash, license state, issue/revocation/contract expiry, network-failure behavior, concurrency limit, next check, and enabled features.

Verify named users by email

With a named-user policy, activation (POST /runtime/v1/activations) needs that user's token (named_user_token, valid for 5 minutes after issue). There are two ways to get it, depending on the seller's plan and how the user is registered.

MethodWho can use itSeller's planWhat the product app does
Email license verificationLocal-ID license users (the seller registered their email address in the directory and did not link them to an OIDC connection)Personal or higherAccept an email address and the emailed code, then get the token with the steps below
Customer portalUsers who sign in with an OIDC connection (their company identity provider)Team or higherAccept the token the user issued in the customer portal (license-user management guide)

Activating without a token returns 401 NAMED_USER_TOKEN_REQUIRED with the available methods in details.available_methods (email for email verification, portal for the customer portal): [] on Evaluation, ["email"] on Personal, and ["email", "portal"] on Team or higher. The product app uses it to decide whether to show the email form or point to the customer portal.

  1. Ask for the license key and email address, then send product_id, license_key, email, device_signing_public_key (the public key of the device signing key you will activate with), and nonce to POST /runtime/v1/email-verifications (optionally locale for the email language). The response is the same signed 202 whether or not the address matched, with verification_id, expires_at, resend_after_sec, and code_length.
  2. Ask for the 6-digit code from the email and send code, device_signing_public_key (the same key as step 1), and nonce to POST /runtime/v1/email-verifications/{verification_id}/confirm. On success, 200 returns named_user_token.
  3. If the user enrolled an authenticator app (two-factor authentication), the response is 401 TWO_FACTOR_REQUIRED. The emailed code is still valid: put the authenticator code (or a recovery code) in second_factor_code and send it again together with the emailed code.
  4. Pass the token as named_user_token in activation (with the official SDK, the named_user_token activation option).
  • A code is sent only when all of these hold. The license key is active, the policy is named-user, the seller's plan is Personal or higher, exactly one active local-ID license user has the address (the same address cannot be registered to more than one local-ID user), and that user has a current assignment on this license. When they do not, the response is the same, so the app cannot tell the difference; this prevents probing for registered addresses. Show something like “If this address is registered, we sent a verification code.”
  • The code has 6 digits, expires in 10 minutes, allows 5 attempts, and works once. A wrong code, device key, or authenticator code each uses one attempt. Sending again with the same device key for the same product, license key, and address makes the previous code unusable (a start with a different device key does not invalidate that device's code).
  • Rate limits (429 RATE_LIMITED with Retry-After): from one source IP, emails to the same address are spaced 60 seconds apart, up to 5 per hour and 10 per day; per license key and source IP, 60 per hour and 300 per day; per source IP, 30 per 10 minutes and 300 per day. When emails for the same license key and address exceed 15 per hour and 30 per day across all source IPs together, the start still returns 202 instead of a 429 but sends no code (this stops several sources from flooding one user). Confirmation allows 60 per 10 minutes and 600 per day per source IP, and authenticator codes 5 per 15 minutes and 10 per day per user. Use resend_after_sec for the resend button and prefer Retry-After on 429.
  • These two requests do not count toward the monthly Runtime API quota. The activation that follows counts once.
  • With the official SDK, Rust Client::start_email_verification / confirm_email_verification (Node.js startEmailVerification / confirmEmailVerification, the same names in Python and Ruby, and usakey_client_start_email_verification / usakey_client_confirm_email_verification in the C ABI) send these requests and verify the signed responses. Start and confirm with the same client you activate with (the same device signing key). The confirmation returns a result instead of throwing (Node.js names; Python and Ruby write them like code_invalid).
    • verified (the token: never store or log it; pass it to activation as namedUserToken right away)
    • codeInvalid and twoFactorInvalid (show remainingAttempts)
    • twoFactorRequired (ask for the authenticator code and send it again together with the emailed code)
    • twoFactorLocked (ask the seller to unlock it)
    • expired (start again)
    • assignmentUnavailable (ask an administrator to check the assignment)
    • rateLimited (wait retryAfter seconds)

    A 429 on the start is an exception (RATE_LIMITED with retryAfter), and that wait takes precedence over resendAfterSeconds. Activation errors tell you a token is needed through availableMethods (available_methods in Python and Ruby, usakey_last_named_user_token_methods() in the C ABI). Only Rust, the C ABI, Node.js, Python, and Ruby have these functions. With other bindings, call the Runtime API directly as above and verify the response like any other signed Runtime API response, including the nonce match (response verification order).

Start a trial from the app (email verification)

Users can start a trial inside the product app without receiving a license key. They enter an email address and the emailed code; Usakey issues a trial license with the trial policy the seller chose and activates the device in the same request. No license key is returned to the user.

Seller setup: create a license-key trial policy for the product. Then, in “Trial sign-ups from the app” on the product's edit page in Console, choose “Trial policy” and “Trials that can start in 24 hours” (default 20, 1–1,000). The product details show how many trials started in the last 24 hours. Trials require the Personal plan or higher.

  1. Send product_id, email, device_signing_public_key (the public key of the device signing key you will use), and nonce to POST /runtime/v1/trial-verifications (optionally locale). The response is always the same 202; when the product does not accept trials (no trial policy chosen, or the 24-hour limit reached), the response is the same and no email is sent.
  2. Send POST /runtime/v1/trial-verifications/{verification_id}/confirm with Idempotency-Key (required), code and email (the same as step 1), and the activation fields (device_signing_public_key (the same key as step 1), device_kem_public_key, device_proof, machine_info, nonce, and optionally machine_signals, app_version, sdk_version, request_id, client_time). Do not send license_key or product_id.
  3. Sign device_proof with the device signing key over the prefix Usakey-Trial-Proof-v1\n followed by the canonical JSON (JCS) of the body without device_proof (a different prefix from activation's Usakey-Activation-Proof-v1\n).
  4. Success is 201 with the same signed data as activation (activation_id, certificate, machine_info_digest) plus license_id. From then on, run periodic checks as for any activation. When retrying after a network failure, send the same Idempotency-Key and the same body to get the same response.
  • Earlier trials are checked at confirmation. If the same address or the same device key had a trial of this product in the last 365 days, the response is 403 TRIAL_ALREADY_USED. If the address still has an active trial, the device joins that trial instead of creating a license (a second device or a reinstall; it does not count as a new trial).
  • When the product's 24-hour limit is reached, the start sends no email and the confirmation returns 403 TRIAL_ACTIVATION_INVALID with details.reason daily_limit. If the seller stopped accepting trials or changed the policy, the reason is not_accepting. A full workspace trial allowance returns 409 PLAN_QUOTA_EXCEEDED; the license and activation are rolled back and the code is used up.
  • The code has 6 digits, expires in 10 minutes, and allows 5 attempts. Rate limits: from one source IP, emails to the same address are spaced 120 seconds apart, up to 3 per hour and 5 per day per product and 10 per day across all products; starts from one source IP, 10 per hour and 30 per day; starts for one product in total, 300 per hour (a start refused by a limit above does not count toward the product total). When emails to the same address exceed 15 per hour and 30 per day across all source IPs together, the start still returns 202 instead of a 429 but sends no code. Confirmation allows 60 per 10 minutes and 600 per day per source IP.
  • The start does not count toward the monthly Runtime API quota. A confirmation that reaches activation counts once.
  • With the official SDK, Rust Client::start_trial_verification / confirm_trial_verification (Node.js startTrialVerification / confirmTrialVerification, the same names in Python and Ruby, and usakey_client_start_trial_verification / usakey_client_confirm_trial_verification in the C ABI) perform the steps above, including the prefix, the Idempotency-Key, and response verification; on success the client is activated. Confirmation results:
    • activated (licenseId and the summary; save the activation ID and run periodic checks as after any activation)
    • codeInvalid (attempts left)
    • expired (start again)
    • trialAlreadyUsed (this address or device already had a trial; offer a purchase)
    • trialUnavailable (reason notAccepting or dailyLimit)
    • rateLimited (wait retryAfter seconds)

    In Rust these are TrialVerificationOutcome::AlreadyUsed and Unavailable. The device limit (MACHINE_LIMIT_REACHED) and the seller's plan limit (PLAN_QUOTA_EXCEEDED) are the same exceptions as for activation. When the result is unknown, the SDK sends the same body with the same Idempotency-Key once more. Only Rust, the C ABI, Node.js, Python, and Ruby have these functions; with other bindings, call the Runtime API directly as above.

Receive the key for a sealed file (sealed files)

The seller can encrypt files shipped with the product (trained models, configuration, data, and so on) so that only devices with a valid license can open them. Each file's data key (AES-256-GCM) is wrapped with HPKE (RFC 9180) to the product's sealing key and stored in the header at the start of the file. The matching private key never leaves Usakey (in production it stays inside a key management service that never releases the key, where one key per environment serves every product; products are told apart by the product ID and key ID in the header, which is part of the wrap's associated data). The device sends only the header to Usakey and, after the license is checked, receives the data key wrapped again to the device's P-256 receiving key. The file itself is never sent to Usakey.

An administrator of a licensed device can still extract the data from memory after the app opens the file. Sealing does not prevent that. Do not put values that must never be seen, such as API keys, on the device, even sealed. Sealing your files does not make analysis or cracking impossible; in principle both remain possible, so use it with that understood. Sealing also does not prove who made a file, so have the app check a hash of the plaintext for files that must not be swapped, such as model weights (example in the manual).

Seller setup: ask your AI assistant, “Use the Usakey MCP skill to turn on sealed files for this product, tell me its sealing key, and seal models/pro.onnx” (turning it on needs “Allow product, policy, and webhook configuration” on the connection; if that is missing, the AI points you to the Console). To do it yourself, press “Turn on sealed files” in “Sealed files” on the product's page in Console, or call POST /mgmt/v1/products/{product_id}/sealed-files in the Management API (the Team plan or higher; it cannot be turned off afterward). Give the one-line public key shown (usakey-sealing-key:v1:…; not a secret) to the sealing tool usakey-seal that comes with the SDK. Add an entitlement name with --entitlement and only devices whose license has that entitlement turned on (true) can open the file (for example, only Pro licenses with pro_model on open the high-accuracy model). The steps are in the user manual.

  1. Send POST /runtime/v1/activations/{activation_id}/sealed-file-keys with the same device signature after registration as a heartbeat (X-Usakey-Activation, X-Usakey-Timestamp, X-Usakey-Nonce, X-Usakey-Signature) and the body {"header":"<base64url of the header bytes>"} (up to 8 KiB). The activation does not change.
  2. In addition to the heartbeat checks, Usakey checks that the license is not suspended (a heartbeat still returns a certificate while suspended, but no key is delivered), that the seller's plan includes sealed files, that the header is well formed and uses a usable key of this product, and that the header's entitlement is JSON true for this license (resolved the same way as for the certificate).
  3. Success is 200 with signed data containing activation_id, file_id, header_sha256, enc, and wrapped_key. wrapped_key is the data key wrapped again with HPKE to the device's receiving public key. It is bound to the activation, the device's receiving key, the product, and the SHA-256 of the header, so another device or another file cannot open it. Verify the response the same way as a heartbeat response.
  • Refusals are 403 SEALED_FILE_ENTITLEMENT_REQUIRED (with the entitlement name in details.entitlement), 422 SEALED_FILE_INVALID, 503 SEALED_FILE_KEY_UNAVAILABLE (retry later), and 403 PLAN_ENTITLEMENT_REQUIRED (with details.entitlement sealed_files). None of them changes the activation. Handle them as in the decision table. Revocation, expiry, suspension, and deactivation return the same codes as a heartbeat.
  • Each request counts once toward the monthly Runtime API quota and shares the rate limit of the activation operations. The device stores the key it receives, so it calls this only the first time it opens a file and after losing the stored key. Do not add it to the periodic check schedule.
  • Fully offline devices receive the keys in the license file when the request file lists the headers in sealed_file_headers (Steps for fully offline devices).
  • With the official SDK, Rust Client::open_sealed_file / open_sealed_file_path check the decision, use the stored key, send and verify this request, and decrypt. They open a file only while the current decision (for the header's entitlement when it names one) allows use, and never return the data key to the app. The keys received are included in the verified state export (export_verified_state), so files open without a connection after a restart. When the SDK learns of a suspension, revocation, expiry, or deactivation, it deletes the stored keys. The C ABI, Node.js, Python, and Ruby have the same function (see each language's README for the name). Only these five have it; with other bindings, do not implement the file format or the key exchange yourself.

Product-side decision table

Keep “exit the app” separate from “disable protected features.” In this table, “disable” means keep screens and settings available while disabling only the licensed feature.

StateHow to identify itBehavior
ActiveValid signatures and bindings, license_status=active, and contract, certificate, and required concurrent-use lease are all validEnable protected features; check again at next_heartbeat_in
SuspendedA signed heartbeat (200) reports license_status=suspended; at first activation, 403 LICENSE_SUSPENDEDDisable immediately, show that the license is suspended, and ask the user to check with the contract administrator. Keep the saved activation ID and continue heartbeats on the normal schedule. After the license is resumed, the same activation ID works again.
Revoked403 LICENSE_REVOKED (heartbeat, first activation, and floating-seat acquire and renew). Revocation also deactivates the activation, but the license state is reported first, so you do not receive ACTIVATION_DEACTIVATEDDisable immediately, show that the license was revoked, and ask the user to activate with another license. Revocation is final: discard the saved activation ID and stop automatic heartbeats.
ExpiredThe certificate's contract expiry (license_expiry) has passed, or 403 LICENSE_EXPIRED (from the moment the term ends; heartbeat, first activation, and floating-seat acquire and renew)Disable immediately and point the user to renewal. Never apply network grace. Expiry does not deactivate the activation, so keep the saved activation ID and continue heartbeats on the normal schedule. When the seller extends the term and resumes the license (including a Stripe renewal payment or converting a trial to a paid license), the same activation ID works again without using another device slot.
Activation deactivated410 ACTIVATION_DEACTIVATED. Returned when an administrator (or a user in the customer portal) deactivated the device, and only while the license is active or suspended (a revoked or expired license returns the codes in the rows above)Discard the saved activation data and ask the user to activate again by entering the license key. The same key can be used. Do not reactivate automatically; reactivation uses a device slot.
Blocked by a risk rule403 ACTIVATION_RISK_BLOCKED / LICENSE_RISK_BLOCKEDDisable and ask the user to contact the seller. Do not retry automatically.
Seller's Usakey subscription inactive402 BILLING_REQUIREDDisable protected features and show “Temporarily unavailable. Contact the seller.” Never ask the purchaser to buy again or re-enter the key.
Seller's plan feature or limit403 PLAN_ENTITLEMENT_REQUIRED, 409 PLAN_QUOTA_EXCEEDED (for example, test products on the Evaluation plan allow 5 devices; a new activation beyond that returns details.quota=device_slots)Keep protected features off on the new device and ask the user to contact the seller. Do not say the user's license was suspended. Heartbeats from devices already activated continue.
Invalid license key404 LICENSE_NOT_FOUNDShow that the key is invalid and ask for re-entry; do not try other keys automatically
Device-key mismatch409 DEVICE_KEY_MISMATCHShow the invalid state and check that saved signing and receiving keys belong to the same activation. Deactivate the old device before re-registering after a key change.
Device limit409 MACHINE_LIMIT_REACHEDShow the invalid state and ask the administrator to deactivate an old device or change the contract. The same code is also returned when the license has reached the number of new devices it can register this month (UTC); the activation response message tells the two apart. Deactivating an old device does not free that allowance, so wait until next month or contact the vendor.
Named-user token or assignment401 NAMED_USER_TOKEN_REQUIRED / NAMED_USER_TOKEN_INVALID, 409 NAMED_USER_MISMATCH, 403 NAMED_USER_ASSIGNMENT_UNAVAILABLEDisable and have the user verify again. If details.available_methods on 401 NAMED_USER_TOKEN_REQUIRED includes email, offer email verification in the product app; if it includes portal, point to issuing an activation token in the customer portal (if it is empty, ask the user to contact the seller). If there is no assignment, ask an administrator to check it. Never switch to another user's token automatically.
Email verification code401 EMAIL_VERIFICATION_CODE_INVALID (details.remaining_attempts), 410 EMAIL_VERIFICATION_EXPIREDThis is not a contract suspension. For a wrong code, show the remaining attempts and let the user enter it again. When the verification expired, ran out of attempts, or was replaced by a newer email, send the email again (steps).
Email verification two-factor authentication401 TWO_FACTOR_REQUIRED / TWO_FACTOR_INVALID (details.remaining_attempts), 403 TWO_FACTOR_LOCKEDFor TWO_FACTOR_REQUIRED, ask for an authenticator app code (or a recovery code) and send it again together with the emailed code, which is still valid. For a wrong code, show the remaining attempts. While locked, ask the user to have the seller unlock it.
Trial403 TRIAL_START_WINDOW_EXPIRED / TRIAL_ALREADY_USED, 422 TRIAL_ACTIVATION_INVALID. When confirming a trial sign-up from the app, TRIAL_ACTIVATION_INVALID is 403 with details.reason not_accepting or daily_limit, and a full workspace trial allowance returns 409 PLAN_QUOTA_EXCEEDEDDisable and offer a purchase. For a trial sign-up from the app, show TRIAL_ALREADY_USED as “this address or this device already had a trial” (another address does not solve it), TRIAL_ACTIVATION_INVALID as “trials are not being accepted right now”, and PLAN_QUOTA_EXCEEDED as “the seller's trial allowance is full”, and ask the user to contact the seller (start a trial from the app).
Concurrency limit409 CONCURRENCY_LIMIT_REACHEDDo not enable the protected feature for this launch; retry later without taking an existing user's concurrent-use lease.
Lease expired410 LEASE_EXPIRED or local expires_at reachedAlways disable floating use and recover only after acquiring a new valid lease.
License unavailable during a lease operation403 LICENSE_UNAVAILABLE (for example, while suspended; a revoked or expired license returns LICENSE_REVOKED / LICENSE_EXPIRED)Disable and confirm the current state with a periodic check.
Wrong flow for the license422 OFFLINE_PACKAGE_REQUIRED / LEASE_NOT_SUPPORTED / OFFLINE_UNMANAGED_HEARTBEAT_DISABLEDRecord it as an implementation error and fix it. For a license that works only offline (OFFLINE_PACKAGE_REQUIRED), guide the user to the offline request file flow.
Machine information required422 MACHINE_INFO_REQUIRED / MACHINE_INFO_REFRESH_REQUIREDResend with the full machine_info (the official SDK does this automatically). Do not show it to the user.
Clock skew or replayed request401 CLOCK_SKEW / NONCE_REPLAYEDNot a contract suspension. Repair the time base with the signed server time from POST /runtime/v1/time, then resend with a new nonce and signature.
Browser origin not allowed403 BROWSER_ORIGIN_NOT_ALLOWEDThe vendor must enable browser authentication in the product form and register its origin. A refused preflight appears as a network error in the browser.
Device signature or identity mismatch401 DEVICE_PROOF_INVALID / ACTIVATION_MISMATCH / INSTANCE_MISMATCHDisable and check the saved keys and activation ID. If that does not fix it, ask the user to activate again.
Malformed request400 INVALID_REQUEST / 413 REQUEST_TOO_LARGE / 422 CERTIFICATE_PAYLOAD_TOO_LARGERecord it as an implementation error and fix it. Do not say the license was suspended.
Nothing to release or deactivate404 NOT_FOUNDWhen releasing a concurrent-use lease or deactivating the device, treat it as done.
Sealed files403 SEALED_FILE_ENTITLEMENT_REQUIRED / 422 SEALED_FILE_INVALID / 503 SEALED_FILE_KEY_UNAVAILABLENone of these changes the activation. SEALED_FILE_ENTITLEMENT_REQUIRED means the license lacks the entitlement the file requires (details.entitlement): turn off only the feature that uses the file and explain. SEALED_FILE_INVALID means the file is for another product, damaged, or sealed with an unusable key: do not retry; ask for the file to be reinstalled. Retry SEALED_FILE_KEY_UNAVAILABLE later.
Network failure, 5xx, or 429No new signed response received (including a connection closed before the response, and a 429 or 5xx whose body is not Usakey error JSON, such as a load balancer or CDN HTML page or an empty body), including 429 RATE_LIMITED and temporary rejections such as 503 PRODUCT_SIGNING_KEY_PENDING / NONCE_STORE_UNAVAILABLE / LEASE_STORE_RECOVERING / LEASE_STORE_UNAVAILABLEUse the network-failure decision below. For heartbeat or lease 429, prioritize Retry-After and create a fresh timestamp, nonce, and signature.
Invalid signature, product/device mismatch, or nonce mismatchResponse or certificate verification failedDo not apply grace; disable and record a security error.

Local decision during network failure

  1. First verify the cached certificate signature, product ID, device binding, and local rollback/tamper state.
  2. If license_status is not active or license_expiry has passed, always disable.
  3. Before certificate expires_at, use is allowed. After it, fail_mode=closed disables, grace continues with a warning until expires_at + network_grace_sec, and open continues with a degraded indicator.
  4. Floating licensing always requires a valid lease regardless of fail_mode. The earliest of lease expiry, certificate/network grace, and contract expiry is the effective limit.

Part 2 · Implementation reference

Issue licenses from purchases

Connect your Stripe account to Usakey to issue, suspend, resume, and revoke licenses as purchasers pay. You receive the money; Usakey receives payment events and changes license state. This is separate from paying your Usakey plan. A paid plan is required for this feature.

Choose one purchase path for each purchase. A normal purchase for a new license can open a static Payment Link from the product app or create a Checkout Session in the seller backend; these are alternatives, not consecutive steps. Only a trial conversion, which keeps the same license, requires the seller backend to safely set the purchaser's license ID.

Separate Stripe credentials by purpose and never put them in the product app. Store only a restricted Usakey integration key (RAK) with the minimum notification permissions in Usakey. A seller backend that creates Checkout Sessions uses a different environment-specific Stripe credential in a secret store, allowed only the operations needed to create sessions, and never registers it with Usakey. Payment Link flows need no Checkout-creation credential. The product app stores neither Stripe credentials nor raw payment data; it sends a pre-purchase claim secret to Usakey and, when available, the Checkout Session ID for matching.

Configure in Console

  1. Create a new, clearly named Stripe Sandbox for the first test. Do not copy production settings; create the keys, webhook, products, and prices in that Sandbox.
  2. In Stripe “Developers” → “API keys”, create a restricted rk_ key dedicated to Usakey. Do not use a creation option such as “Providing this key to another website” that turns on many permissions at once; set only these five individually: read access to Checkout Sessions, Subscriptions, Invoices, and Events plus write access to Customers. Do not register an sk_ key or the backend credential.
  3. In Usakey, open “Development & integrations” → “Stripe integration” and register the notification URL shown there as a Webhook in the same Sandbox, then save the displayed whsec_ and restricted key in Usakey.
  4. Create the product and price in the same Sandbox and map it to the Usakey product, policy, and term. Never mix price IDs from production or another Sandbox.

The RAK reads Events only to re-verify notifications recorded before a Usakey handler update, and writes Customer metadata only to store usakey_license_id and usakey_license_status. usakey_license_status reflects the state when the purchase was applied and is not updated when the license is later suspended or revoked. The RAK does not edit names, email addresses, or payment methods and is never reused for Checkout creation.

If Events read access was missing during a handler update and a full-refund notification could not be verified, Usakey suspends every active or expired license purchased through this integration in its current mode, because the refunded purchase cannot be identified, and shows a Console notice. Until this is resolved, new purchases are held (Usakey returns a temporary error so Stripe retries) and the Stripe mode cannot be changed. Add Events read access to the same restricted key or replace the key, then run “Reconcile pending and quarantined events” in Stripe integration. You can run it repeatedly: refunded purchases are revoked, and licenses stopped by mistake are resumed after the current contract state is confirmed.

Use Dynamic Payment Methods rather than fixing payment_method_types. For subscriptions, use a recurring Billing price and mode=subscription. With Stripe Tax, set automatic_tax[enabled]=true and register the applicable tax regions with Stripe; automatic calculation does not replace tax registration.

After opening the target Sandbox, use these Stripe Dashboard pages: API keys, Webhooks, and the Product catalog. These links may retain the environment currently selected in Stripe Dashboard. Before creating or saving anything, confirm that the Sandbox indicator and Sandbox name at the top of the page match your target and that you are not in production. If they do not match, switch to the target Sandbox from the account selector and reopen the link.

A tenant can have one Stripe integration. Edit an existing integration only when moving from test to production, and replace both the production RAK and webhook secret. Test price mappings stop at the switch, so map production Price IDs afterward. Production-to-test switching is not supported; use a separate tenant for continued Sandbox testing. If a production event reaches a test integration, Usakey returns a temporary error without finalizing its history so Stripe can retry after the integration is corrected. If a test event reaches a production integration, Usakey does not change a license and records the reason in history.

Restricted keys (rk_) can be created only in Stripe Dashboard, not through the Stripe API or Stripe CLI. A person with browser access to the target Sandbox must create the key manually. End-to-end notification testing also requires a real Sandbox account because Usakey queries the Checkout Session through the Stripe API; a mocked webhook cannot verify this flow.

Webhook delivery during localhost development

Stripe cannot notify localhost directly. Use stripe listen --forward-to or a public tunnel such as ngrok. Register the fixed whsec_ printed by stripe listen. Usakey processes these events; select them when registering the webhook in Stripe Dashboard or with stripe listen --events:

checkout.session.completed
checkout.session.async_payment_succeeded
checkout.session.async_payment_failed
invoice.paid
invoice.payment_failed
customer.subscription.updated
customer.subscription.deleted
charge.refunded
stripe listen --forward-to <Usakey webhook URL> \
  --events checkout.session.completed,checkout.session.async_payment_succeeded,\
checkout.session.async_payment_failed,invoice.paid,invoice.payment_failed,\
customer.subscription.updated,customer.subscription.deleted,\
charge.refunded

Keep stripe listen running through Checkout. Stripe CLI sign-in (stripe login) opens an issued URL for approval in a regular browser. Forwarded events may arrive out of order; Usakey absorbs ordering differences and reconciles the current Subscription through the Stripe API.

Implement in the product app

Usakey receives Stripe webhooks, so the product app does not need a webhook endpoint.

The official SDK's ClaimRequest / ClaimClient (usakey::claim in Rust, usakey_claim_request_generate, usakey_claim_once, and usakey_claim_acknowledge in the C ABI, and classes of the same names in Node.js, Python, and Ruby) generate the verifier, challenge, and Idempotency-Key, claim the key (results delivered, pending, finished, retryLater), send the acknowledgement, and compute the next attempt time (up to 17 days after the request is created). Save the claim request in the StateStore described above.

  1. Before opening checkout, generate and securely store a claim verifier and Idempotency-Key.
  2. Attach only the verifier hash to Checkout; never send the verifier itself to Stripe or the seller.
  3. After payment, send the verifier to the claim endpoint and securely save the key. Send the Session ID when available; if the redirect is lost, recover with the verifier alone. Resume delayed payment polling after restart.
  4. After confirming persistence, send the ACK with the same Idempotency-Key.

If a 201 response is lost before ACK, retrieve the same key with the same Idempotency-Key. Do not change it.

Claim a license key

  1. Generate 32 bytes from a cryptographically secure random source, encode them as 43 unpadded Base64URL characters, and store the claim_verifier locally before purchase. Generate a separate claim Idempotency-Key; persist the challenge, verifier, and Session ID when available unchanged until ACK.
  2. Compute base64url(SHA-256(UTF-8 bytes of claim_verifier)) and prefix it with usk_claim_v1_. The hash is not secret, but never put the verifier itself in Stripe, the seller backend, or a URL.
  3. Set the challenge in exactly one place: metadata[usakey_claim_challenge] when the seller backend creates Checkout, or ?client_reference_id=usk_claim_v1_... when the app opens a static Payment Link.
  4. Put ?session_id={CHECKOUT_SESSION_ID} in the backend success_url, or in the Payment Link “After payment” redirect URL set in Stripe Dashboard (after_completion.redirect.url in the API). Pass it to the product app from a purchase-complete page that authenticates the purchaser. If the redirect is lost and no Session ID is available, the stored verifier alone still recovers the claim.

Switch an existing Stripe integration only after updating every purchase path. Add the challenge before purchase begins in the product app and in every Payment Link or backend Checkout flow, then choose “Enable secure delivery” once in Stripe integration. Sessions started before the switch can use the legacy flow for up to 72 hours after key issuance. New purchases require claim_verifier; this switch cannot be undone. New integrations require secure delivery from the start and do not show this button.

POST /integrations/stripe/{claim_token}/license-claims
Idempotency-Key: <16–128 chars of A-Z a-z 0-9 - _, fixed until ACK>
Content-Type: application/json

{"claim_verifier": "...43-chars...", "checkout_session_id": "cs_test_..."}

When the challenge was attached before checkout, checkout_session_id is optional. If the redirect is lost, send the 43-character verifier alone to resume the claim within the same integration. When available, send the Session ID too and verify that both identify the same purchase. If a Session ID is supplied, both values must match; do not search by Session ID alone or switch to another verifier. Only legacy pre-switch purchases require a Session ID.

claim_token is the fixed ID shown in Stripe integration and stored as an environment-specific product setting. It is not a per-user secret. Regenerating the webhook URL does not change the claim token or claim URL, protecting shipped apps and delayed purchases.

A successful claim returns 201:

{
  "license_id": "lic_...",
  "license_key": "USK_TEST-...",
  "status": "active",
  "expires_at": "2027-03-31T23:59:59+09:00"
}

expires_at is ISO 8601 with a UTC offset and is null for perpetual licenses. status can be other than active, such as suspended. Keys for test products start with USK_TEST-, and keys for production products start with USK-.

Never ACK before confirming persistence. Write the key to a secure destination and confirm it with flush/fsync plus atomic rename, or a successful OS credential-store write. Until ACK and for 72 hours after issuance, a lost 201 can be recovered with the same verifier, Idempotency-Key, and (if first sent) Session ID. Then send ACK to delete Usakey's temporary ciphertext.

POST /integrations/stripe/{claim_token}/license-claims
Idempotency-Key: <same value used for claim>
Content-Type: application/json

{"claim_verifier": "...43-chars...", "checkout_session_id": "cs_test_...", "acknowledge": true}

ACK succeeds with 204 No Content. Resend the same body if its response is lost; processed ACKs also return 204. After a valid 201 was persisted, ACK converges to 204 without showing the key even after the 72-hour retrieval window or cleanup. Keep the verifier and Idempotency-Key until ACK completes because retrieval expires.

The claim verifier is the credential. Its 43 characters alone retrieve a new key, so protect it like a license key. Treat the Session ID as temporary sensitive data too. Authenticate the purchaser on the completion page and never send the Session ID, verifier, or license key to analytics, access logs, error logs, or Referrer headers. Remove the Session ID from the URL immediately with history.replaceState.

Handle each response

ResponseMeaningProduct-app behavior
201Key issued; waiting for ACKPersist it safely, then ACK with the same Idempotency-Key. If the response was lost, retrieve with the same key.
204ACK acceptedTemporary data is deleted; continue to normal device activation with the saved key.
400 INVALID_REQUESTNeither a Session ID nor a well-formed 43-character verifier was sent; acknowledge is not a JSON boolean; or, once the purchase is found, the Idempotency-Key is missing or is not 16–128 characters of A–Z, a–z, 0–9, -, and _Fix the integration and keep secrets and Session IDs out of logs.
404 NOT_FOUNDWrong claim URL (claim_token), the seller has stopped the integration, no matching data, payment still settling, or verifier/Session ID missing, malformed, or mismatched; for ACK, an ACK sent before the 201 or a correctly formed but different keyFor claim, retry with the same values during the window below. For ACK, confirm the original key.
413 REQUEST_TOO_LARGEThe request body is too large (up to 1 MiB)Fix the integration.
429 RATE_LIMITEDToo many requests from one address (30 per minute)Wait Retry-After seconds, then retry with the same values. Do not stop retrying.
410 ALREADY_CLAIMEDAlready ACKed or claimed with another keyUse the saved key; if it was not saved, contact the seller.
410 NOT_REQUIREDNo new key was issuedNot an error; see trial conversion below.
410 REVOKEDThe license is revoked (cancellation, full refund, or revocation by the seller)Do not retrieve a key; stop automatic retries for this purchase.
410 EXPIREDClaim window expired (72 hours after key issuance; after that, this code is returned even if the key was claimed)Direct the purchaser to contact the seller.

Do not stop retrying a claim 72 hours after purchase. Some Dynamic Payment Methods can take about 14 days to settle. A 404 cannot distinguish missing data, pending settlement, verifier mismatch, or verifier/Session mismatch. Retry every few minutes after purchase, then retry less often with increasing waits (up to a cap), and resume after restart. Decide by code, not by message. Keep the challenge, verifier, Idempotency-Key, and available Session ID in secure local storage for at least 17 days (up to about 14 days of settlement plus 72 hours after key issuance) or until ACK. Stop only after a failed payment is confirmed, the key is persisted and ACKed, or NOT_REQUIRED, REVOKED, EXPIRED, or ALREADY_CLAIMED is returned.

Convert a trial

When a trial user purchases, the license key does not change. Set the existing trial license ID in Checkout metadata metadata[usakey_license_id]; Usakey updates only the same license's policy and expiry. client_reference_id is for the claim challenge, not the license ID. The claim endpoint returns NOT_REQUIRED, so the app continues with its saved key.

Usakey converts only when the ID identifies an unconverted trial (active or expired) for the same product as the purchased price. Otherwise (a typo, another product, or an already converted trial) Usakey issues a new license and key, and the claim returns 201. A matching trial that is suspended or revoked is neither converted nor replaced; the reason is recorded in the Stripe integration history. The claim then keeps returning 404, so check the history and contact the purchaser.

  1. Store the license_id returned when issuing the trial and bind it to the purchaser in the seller backend. Use the lic_... ID, not the key.
  2. When purchase starts, create Checkout with the saved ID in metadata[usakey_license_id] and the app challenge in metadata[usakey_claim_challenge]. Do not trust an arbitrary ID from the app or browser; confirm that it belongs to the signed-in purchaser's active trial.
  3. Put ?session_id={CHECKOUT_SESSION_ID} in success_url. For desktop apps, pass it through a product-specific deep link or display it as a reference number the user can paste into the app.
  4. Send the saved verifier, Idempotency-Key, and available Session ID. On NOT_REQUIRED, do not wait for a new key; run the normal periodic check to receive the updated policy and expiry.

Trial conversion needs a seller backend to set each purchaser's ID safely. A static Payment Link can issue a new license but cannot replace this conversion flow.

State changes after purchase

Usakey automatically reflects failed payment and cancellation in license state. From the product app's perspective, only the periodic license-check result changes; no separate purchase-specific decision logic is needed. Expiry and suspension keep device activations, so the same devices work again after payment or resumption (see below).

Payment confirmed or contract renewed (Stripe active / trialing)
Active. The expiry follows the end of the Stripe billing period (it can become earlier)
Renewal payment failed, pending, or paused (past_due / unpaid / incomplete / paused)
Suspended
Renewal payment not confirmed by the expiry
Expired until the payment is confirmed. Device activations are kept, so the same devices work again once the license is active after payment
Contract ended (canceled / incomplete_expired)
Revoked. A revoked license is not resumed
Full refund of the purchase (a one-time payment or a subscription's first invoice)
Revoked; a partial refund does not change state
Full refund of only a subscription renewal (the subscription continues)
Suspended, not revoked. If the subscription continues, the next Stripe notification resumes the license to match the subscription; if it was canceled, the license is revoked
A license the seller suspended in the Console or Management API
Not resumed when a payment is confirmed (only its expiry and policy follow Stripe). The seller resumes it

For a first purchase, Usakey issues no license until payment is confirmed. Duplicate notifications do not issue duplicate licenses. Usakey records processed notifications, and you can review the result in the history under Stripe integration.

Stripe notifications may arrive out of order. For subscription notifications, Usakey does not move state backward based only on the event name; it fetches the latest Subscription from Stripe and resolves it to active, suspended, or revoked. If a cancellation or full-refund notification arrives before the purchase record, Usakey holds it as pending, matches it when the purchase data arrives, and converges the license to revoked. A full refund whose purchase data still has not arrived after 35 days (the longest period in which Stripe can redeliver a notification is 30 days) is treated as a payment that does not belong to a Usakey purchase: Usakey stops waiting and records the reason in the history. A full refund that is still waiting and has no purchase record yet does not block switching the Stripe environment from test to production; waiting items from the previous environment are closed at the switch.

Part 2 · Implementation reference

Fully offline devices

A device that can never connect to Usakey exchanges an activation request file .usakeyreq created on the target device and an entitlement file .usakeylic issued by an administrator.

The official SDK creates the request file and verifies and imports the entitlement. Add product-app actions to export/import the files and show the result. The administrator receives the request in Console and issues the entitlement.

Step 1: Create .usakeyreq on the target device

Add “Create offline activation request” to the product app, call the SDK function below, and save the returned bytes. Never overwrite an existing file with the same name; ask the user to choose another name.

Client::export_offline_activation_request
Rust (offline feature); creates the signed request content.
export_offline_request
The same function for each language's ; see that language's README for naming.
  1. Target device: The SDK signs normalized JSON containing the device signing key, P-256 receiving key, product ID, fingerprinted device information, one-time nonce, and creation time. It never contains the license key or private keys. Requests older than seven days or more than five minutes in the future are rejected at issuance.
  2. Online administrator: Open the target in Licenses, choose “Use on a device that cannot connect to the Internet”, upload .usakeyreq, and download the issued .usakeylic. The request has no license key and is bound to the license open in Console.
  3. Target device: Carry the .usakeylic on removable media and import it in the product app (Step 2).

Step 2: Import the issued .usakeylic

Add “Import offline entitlement” and pass the file contents to the SDK. It reads trust-bundle.json, manifest.paseto, and certificate.paseto (and named-user-proof.paseto for named-user licenses) from the ZIP, then verifies the , manifest signature, every file size and SHA-256, certificate signature, product/license/device-key binding, issue expiry, and contract expiry. Reject the package if any check differs.

Client::import_offline_activation_package
Rust; verifies the file and enables use only on success.
Client::import_and_store_offline_activation_package
Rust; stores a verified package in encrypted OfflineLicenseStore.
Client::load_stored_offline_activation_package
Rust; reads the stored package after restart and verifies it from the beginning each time.
import_offline_package
The language-binding import function. Call it at most once, on a newly opened client, before any online operation. A second call on the same client, even after a failed import, fails with OfflineImportUnavailable (USAKEY_STATUS_OFFLINE_IMPORT_UNAVAILABLE in the C ABI). The one exception is a call whose expected named-user IDs are malformed: it is refused before the package is read and does not use up the import. After a restart or a failure, open a new client and call it again.

For named-user licenses, always pass the expected user and assignment when importing. The functions above reject such a package. In Rust, pass OfflineNamedUserIdentity::new(assignment ID, license user ID) to Client::import_named_user_offline_activation_package (import_and_store_named_user_offline_activation_package to store it, load_stored_named_user_offline_activation_package after a restart). Language bindings take the same pair as the second argument of import_offline_package (expected_named_user of usakey_client_import_offline_package in the C ABI). You can look up the assignment ID (las_…) and license user ID (lsu_…) with the Management API GET /mgmt/v1/licenses/{id}/assignments.

Before it expires, create a new request file on the same device and issue again. Reissuing for the same device (the same device keys) does not use another device slot or issuance. Store the complete issued file, including its signed key list, certificate, and manifest. Never store a device private key or license key in it. Use a per-user application-data directory that other users cannot read or write.

The keys for sealed files can also be delivered in the license file. List the headers of the sealed files in the optional request field sealed_file_headers (an array of base64url strings of the header bytes, 1–16 entries, no duplicates; covered by the device_proof signature). At issuance, the plan, header, product, key, and entitlement are checked for each file, and the license file carries sealed-file-keys.json (listed in the manifest). If the key for even one file cannot be delivered, nothing is issued and the reason is shown in Console and the Management API. Only the SHA-256 of each header, not the header itself, is kept with the request. After import, the device opens those files without a connection (Receive the key for a sealed file).

After restart

On a fully offline device, import the stored entitlement at every launch and allow use only after a fresh full verification.

  • Never allow use past the contract expiry.
  • The earlier of manifest and certificate expiry is the absolute limit; network grace cannot extend it.
  • Floating licensing is not available in fully offline mode.
  • Never restore a previous approval from a failed or expired file.

.usakeyreq JSON example

{
  "format": "usakey-offline-request-v1",
  "product_id": "prod_...",
  "device_signing_public_key": "ed25519:...",
  "device_kem_public_key": "p256:...",
  "machine_signals": { "installation": "<SHA-256 hex>" },
  "machine_info": { "os": "linux", "arch": "x86_64" },
  "sdk_version": "usakey-rust/0.1.0",
  "app_version": "1.0.0",
  "nonce": "<16-byte random Base64URL>",
  "request_id": "<UUID>",
  "generated_at": 1787623200,
  "display": { "product_name": "<product-name>", "device_code": "A1B2C3D4E5F6" },
  "device_proof": "<Ed25519 signature Base64URL>"
}

machine_signals and display appear only when set through the Rust SDK's OfflineActivationOptions; request files created by the language bindings omit them. The 12-character device code returned by the SDK and the 8-character comparison code in the issued manifest (XXXX-XXXX) are both prefixes of the SHA-256 of the same device signing public key (the 8-character code equals the first eight characters of the 12-character code).

The request is a JSON object no larger than 64 KiB in format usakey-offline-request-v1. Keep device information to the minimum needed for authentication; large arrays and deep nesting are rejected. At most 10 license files are issued to one device in 24 hours. Beyond that, even a new request file is refused (the Management API answers 422 OFFLINE_PACKAGE_INVALID with details.reason=package_limit_reached); try again later. Sending the same request file again returns the license file that was already created.

The device_proof input uses the same signed payload as first activation: Usakey-Activation-Proof-v1\n + JCS(payload) without device_proof itself. Do not include raw serial numbers or the complete license key. The issued manifest contains a product name, the license ID and part of the license key, an eight-character device comparison code, and the issue and expiry times. Fully offline mode cannot enforce concurrency, pre-expiry revocation, or real-time anomaly detection; it is limited to device-bound use until certificate expires_at.

Part 2 · Implementation reference

Response verification order

Verify in this order that a Usakey response came from an authorized source and was not changed in transit.

Shipped with the product

Initial trust anchor

Never fetch this key over the network; embed it safely in the product distribution. Console's connection-settings download contains it.

Public-key list

Verify Usakey signing keys

Check list signature, expiry, and sequence to prevent replacement with an older list.

Short-lived-key delegation

Verify response public key

Check the product long-term-key signature, purpose, product ID, and six-to-24-hour validity.

API response

Match body and signature

Check product ID, one-time nonce, HTTP status, body fingerprint, and short-lived-key signature.

Product app

Use only after verification

Also verify product, device, and expiry in the entitlement before enabling features.

Figure 4: Do not trust a public key from the network unconditionally. Build the chain of trust from the key embedded in the product.
  1. Use the embedded initial trust anchor to verify the signature and expiry of the public-key list (API name: Trust Bundle).
  2. Use the product long-term public key identified by parent_key_id to verify signing_delegation, then check product ID, purpose, online_key_id, and six-to-24-hour validity.
  3. Confirm that response_assertion matches the product, parent key, short-lived key, nonce, and HTTP status.
  4. Canonicalize the decision, compute SHA-256, and compare it with body_sha256.
  5. Verify response_signature with the delegated short-lived Ed25519 public key. This proves that Usakey generated the response and that it was not changed in transit.

Canonical JSON rules

  • Sort object keys as strings in ascending order and reject duplicate keys.
  • Keep array order and omit whitespace between elements and around colons.
  • Allow integers only; reject floating-point numbers.
  • Escape valid UTF-8 strings as JSON and use true, false, and null for their JSON values.

Example: {"b":2,"a":["x",true]} becomes {"a":["x",true],"b":2}.

When a development Usakey server signs itself (signing_mode=development_self_signed), its public key is for development. Never ship it as the initial trust anchor in a distributed product.

Appendix

Glossary

This page uses English names and includes API names in parentheses where helpful. Program logic should use the API names and stable identifiers.

Runtime API (product app integration)
The API that distributed product apps use for device activation and license-state checks. This page describes it. The API your own systems use to operate Usakey is the Management API, described on the external integration page.
Device activation
The API name is activation: associating a license with a device key and permitting use on that device.
Periodic license check
The API name is heartbeat: a request that checks whether a running device may still use the license.
Floating seat
The API name is lease: a short-lived unit reserved by a running process or device under a concurrent-use license.
Trust Bundle
A signed public-key list used by the product app to verify Usakey responses.
Product long-term key
A product-level public key in the Trust Bundle used to verify certificates and short-lived online-key delegation. Paid plans use a product-specific non-extractable key; Evaluation uses a product-specific encrypted key approved by a shared issuing key.
Short-lived online key
A key valid for six to 24 hours that signs Runtime responses. Verify its delegation by the product long-term key before use.
Public-key-list sequence
The API field is sequence: reject a list with a lower number than the last accepted list to detect rollback.
One-time nonce
The API field is nonce: fresh random data for each request that prevents replay.
Body fingerprint
SHA-256: a 64-character value derived from content; one changed byte produces a different value. It is not encryption.
Canonical JSON
JCS: rules that remove whitespace and key-order differences so every environment computes the same JSON representation.
Base64URL
A format for representing binary data with characters safe for URLs and HTTP. It is not encryption.
C ABI
A common calling convention that lets languages invoke the same Rust SDK core.
Binding
Thin bridge code that lets a language call the Rust SDK core; the core verifies signatures and evaluates licenses.
Fail closed
Disable protected features when state cannot be verified rather than allowing use without proof.
Activation request file
The .usakeyreq signed request file created by a fully offline device; it contains no license key or private key.
Entitlement file
The .usakeylic signed file issued from a request in Console and usable only by its target device.

Appendix

Error format

Errors return the following JSON with an HTTP status. This non-2xx JSON is not a signed success response. Verify HTTPS and the JSON structure before handling it. Use API code for program branches, message for logs and investigation, and request_id to match support requests. doc_url links to this documentation.

{
  "error": {
    "code": "LICENSE_EXPIRED",
    "message": "The license term has ended.",
    "request_id": "…",
    "retryable": false,
    "doc_url": "https://usakey.jp/docs#errors",
    "details": {}
  }
}

The stable error code exposed by the official SDK is available only from Rust Error::stable_api_code() (or usakey::session::take_runtime_error_code() on the same thread right after a BlockingSession failure), C ABI usakey_last_runtime_error_code(), Python RuntimeFailureError.code, Ruby RuntimeFailureError#code, Node.js / TypeScript RuntimeFailureError.apiCode, and PHP RuntimeFailureException::$apiCode. Other s should use only the exposed status, failure kind, and retry hint. Do not parse message strings or branch on a code the binding does not expose. The SDK ZIP's docs/support-matrix.md lists available fields; the skills ZIP's references/runtime-protocol.md classifies stable codes.

When retryable is true, the same action may succeed after waiting, but retry behavior is operation-specific. Current cases are RATE_LIMITED, IDEMPOTENCY_IN_PROGRESS, RATE_LIMIT_STORE_UNAVAILABLE, and PRODUCT_SIGNING_KEY_PENDING. For a Management API signing-key delay, wait a few minutes and retry with the same body and Idempotency-Key. Offline-file issuance uses no idempotency key; retry with the same request file and options. Runtime API signing-key delay returns HTTP 503 and Retry-After. For authenticated heartbeat or lease retries, generate a current timestamp, new nonce, and new signature; never resend the same byte sequence.

The exception is an unknown result after sending first activation. Let the official SDK retry once with the same raw byte body and same Idempotency-Key. Rebuilding the body changes its digest and causes an idempotency conflict. If a user or operator explicitly starts recovery after the SDK reports an unknown result, use a new body and new key.

Do not retry deterministic denials such as LICENSE_EXPIRED or ACTIVATION_DEACTIVATED; show the state using the Product-side decision table.

Check HTTP responses and error JSON for each Runtime and Management endpoint listed in OpenAPI in the API reference. Use the skills ZIP material above for stable-code classifications.