API version 2026-09-21
API referenceProduct 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
- Understand the complete model in How authentication works
- Prepare settings and keys in Prepare these values first
- 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.
-
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.
-
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/mcpOpen
/mcpin Claude Code, chooseusakey, and press “Authenticate” to open the browser. From a terminal, you can also runclaude mcp login usakey. You are done whenclaude mcp get usakeyshows✔ Connected. - Codex
-
codex mcp add usakey --url https://usakey.jp/mcpAdding 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 isOAuthincodex 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.
| Capability | Example request | Required 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.
- Download the ZIP, verify its checksum, and extract it.
- Move the two folders directly under the ZIP root (
usakey-auth-integrationandusakey-stripe-integration) to your AI's skill directory. Do not create a duplicate same-named folder. - Confirm that
SKILL.mdandVERSIONexist at the final paths, reload the AI, and invoke the skill by name.
Save the ZIP and checksum in the same folder, then run the following before extracting.
The example is for Linux and macOS. On Windows, use PowerShell Get-FileHash -Algorithm SHA256 to compare the value at the start of the sidecar file.
curl -fSLO https://usakey.jp/downloads/usakey-skills.zip
curl -fSLO https://usakey.jp/downloads/usakey-skills.zip.sha256
if command -v sha256sum >/dev/null 2>&1; then
sha256sum -c usakey-skills.zip.sha256
else
shasum -a 256 -c usakey-skills.zip.sha256
fi- Codex
- The final paths are
~/.codex/skills/usakey-auth-integration/SKILL.mdand~/.codex/skills/usakey-stripe-integration/SKILL.md. Invoke them with$usakey-auth-integrationand confirm that the AI recognizes the skill. - Claude Code
- The final path is
~/.claude/skills/usakey-auth-integration/SKILL.mdfor a personal skill or.claude/skills/usakey-auth-integration/SKILL.mdfor a project skill. Put the Stripe skill beside it and invoke it with/usakey-auth-integration. - Other assistants
- For an AI without automatic skill loading, attach the Markdown files from the ZIP to the conversation or place them in the repository and ask the AI to read them.
After placing the skills for Codex, you can verify the required files from a command line.
test -f ~/.codex/skills/usakey-auth-integration/SKILL.md && \
test -f ~/.codex/skills/usakey-auth-integration/VERSION && \
test -f ~/.codex/skills/usakey-stripe-integration/SKILL.mdWhen installed from the ZIP, run the SDK fetch script from the skill's absolute path while your shell is at the product repository root. Pass the environment's distribution URL as an argument.
cd /path/to/your-product
# Personal Codex installation
~/.codex/skills/usakey-auth-integration/scripts/fetch-sdk.sh https://usakey.jp
# Personal Claude Code installation
~/.claude/skills/usakey-auth-integration/scripts/fetch-sdk.sh https://usakey.jpFor updates, replace the same-named folders with the folders from the new ZIP instead of extracting over the existing folders. The skill version is USAKEY_SKILL_VERSION in VERSION. You don't need to fetch the SDK again while the vendored version is at or above USAKEY_SDK_MIN_VERSION in the same file. The README in each skill repeats these instructions; verify the checksum at /downloads/usakey-skills.zip.sha256.
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.
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
| Actor | Responsibility | Holds |
|---|---|---|
| Administrator | Sets contract terms and issues, suspends, resumes, or revokes licenses. | Console account |
| User | Enters the license key received from the administrator when launching the product for the first time. | License key |
| Product app | Generates device keys, checks license state, and enables or disables protected features. | Product ID, device private keys, verified certificates |
| Usakey | Evaluates 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.
-
Administrator → user
Give the license key
Send the USK-… issued in Console to the user through a secure channel.
-
Product app (on device)
Generate two key pairs
Generate a signing key and a data-receiving key. Neither private key leaves the device.
-
Product app → Usakey
Request registration
Send the license key, product ID, two public keys, and a one-time nonce, with a device signature.
-
Usakey
Verify the contract and device
Check the key, product, device signature, and remaining device capacity.
-
Usakey → product app
Return a signed entitlement
Return the activation ID and a certificate containing state, expiry, and usage terms.
-
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.
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.
| Plan | Minimum device-state interval | Scheduled 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.
| Result | Product-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 mismatch | Do not apply network grace. Disable the feature and record a security error. |
| Cannot connect or temporary server error | Continue 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.
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.
01 / Target device
Create an activation request
.usakeyreqIt contains public keys and a device signature, never the license key or private keys.
02 / Administrator
Issue
.usakeylicin ConsoleBind the request device to the license and create a signed entitlement with an expiry.
03 / Target device
Import
.usakeylicVerify 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
| Check | How it is checked | Risk addressed |
|---|---|---|
| Did it come from a registered device? | Verify the device-private-key signature with the registered public key | A request from another device that copied only the license key |
| Is this a replay? | Verify time and the one-time nonce generated for each request | Resending a previously successful request |
| Is the response from Usakey? | Verify response and certificate signatures from the trust anchor shipped with the product | A fake server granting unauthorized use |
| Was the response body changed? | Compare the body hash, product ID, request nonce, and HTTP status with the signed assertion | Changing 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 certificate | Using 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 action | Online device | Fully offline device |
|---|---|---|
| Suspend, resume, or revoke | The 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 expiry | The certificate contains the expiry, so the license never remains valid beyond it even without a connection. | Use the expiry inside .usakeylic. |
| Force-deactivate a device | The 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 features | The 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.
- Download the ZIP and checksum, verify them, and extract the SDK into
vendor/usakey-sdk/in the product repository. - 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. - 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 ;;
esacUSAKEY_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 (
DeviceIdentityworks 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.
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 requiresedition2024, use a Cargo version newer than the MSRV, such as 1.89. - The Cargo package is
usakey-sdkand the crate isusakey. Add it toCargo.tomlas follows.
[dependencies]
usakey = { package = "usakey-sdk", path = "vendor/usakey-sdk/rust/usakey", features = ["blocking-session"] }- The
blocking-sessionfeature is required forusakey::sessionand its synchronous APIs such asBlockingSession,MachineInfo, andtake_runtime_error_code. Omit it when using only async APIs. Seerust/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.
- For Rust, roll only the state file back while retaining the rollback record and confirm
encrypted state rollback detected. - 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.
- Store
clock_snapshotand 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
AAECAwQFBgcICQoLDA0ODwis 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.
| Operation | data fields | Save in the product app |
|---|---|---|
First activation201 | activation_id, certificate, machine_info_digest | Activation ID, verified certificate, and hash of the submitted machine information |
Periodic license check200 | activation_id, license_status, certificate, next_heartbeat_in, machine_info_digest | New certificate, seconds until next check, and the server-side machine-information hash |
Acquire floating seat201 | lease_id, instance_id, expires_at, heartbeat_in | Lease ID and expiry |
Renew floating seat200 | lease_id, instance_id, expires_at, heartbeat_in | Updated expiry |
Release floating seat200 | lease_id, status="released" | Discard the local lease |
Start email verification202 | verification_id, expires_at, resend_after_sec, code_length | The verification ID (used to send the code). Use the resend seconds for the screen |
Email verification200 | named_user_token, expires_at | Do not store it; pass it to activation as named_user_token right away |
Start a trial from the app202 | verification_id, expires_at, resend_after_sec, code_length | The verification ID (used to send the code) |
Trial confirmation (issue and activate)201 | activation_id, certificate, machine_info_digest, license_id | Same as first activation. No license key is returned |
Server time200 | server_time, sdk_version | Verified 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.
| Method | Who can use it | Seller's plan | What the product app does |
|---|---|---|---|
| Email license verification | Local-ID license users (the seller registered their email address in the directory and did not link them to an OIDC connection) | Personal or higher | Accept an email address and the emailed code, then get the token with the steps below |
| Customer portal | Users who sign in with an OIDC connection (their company identity provider) | Team or higher | Accept 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.
- 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), andnoncetoPOST /runtime/v1/email-verifications(optionallylocalefor the email language). The response is the same signed202whether or not the address matched, withverification_id,expires_at,resend_after_sec, andcode_length. - Ask for the 6-digit code from the email and send
code,device_signing_public_key(the same key as step 1), andnoncetoPOST /runtime/v1/email-verifications/{verification_id}/confirm. On success,200returnsnamed_user_token. - 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) insecond_factor_codeand send it again together with the emailed code. - Pass the token as
named_user_tokenin activation (with the official SDK, thenamed_user_tokenactivation 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_LIMITEDwithRetry-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 returns202instead of a429but 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. Useresend_after_secfor the resend button and preferRetry-Afteron429. - 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.jsstartEmailVerification/confirmEmailVerification, the same names in Python and Ruby, andusakey_client_start_email_verification/usakey_client_confirm_email_verificationin 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 likecode_invalid).verified(the token: never store or log it; pass it to activation asnamedUserTokenright away)codeInvalidandtwoFactorInvalid(showremainingAttempts)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(waitretryAfterseconds)
A 429 on the start is an exception (
RATE_LIMITEDwithretryAfter), and that wait takes precedence overresendAfterSeconds. Activation errors tell you a token is needed throughavailableMethods(available_methodsin 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 thenoncematch (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.
- Send
product_id,email,device_signing_public_key(the public key of the device signing key you will use), andnoncetoPOST /runtime/v1/trial-verifications(optionallylocale). The response is always the same202; 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. - Send
POST /runtime/v1/trial-verifications/{verification_id}/confirmwithIdempotency-Key(required),codeandemail(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 optionallymachine_signals,app_version,sdk_version,request_id,client_time). Do not sendlicense_keyorproduct_id. - Sign
device_proofwith the device signing key over the prefixUsakey-Trial-Proof-v1\nfollowed by the canonical JSON (JCS) of the body withoutdevice_proof(a different prefix from activation'sUsakey-Activation-Proof-v1\n). - Success is
201with the same signeddataas activation (activation_id,certificate,machine_info_digest) pluslicense_id. From then on, run periodic checks as for any activation. When retrying after a network failure, send the sameIdempotency-Keyand 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_INVALIDwithdetails.reasondaily_limit. If the seller stopped accepting trials or changed the policy, the reason isnot_accepting. A full workspace trial allowance returns409 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
202instead of a429but 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.jsstartTrialVerification/confirmTrialVerification, the same names in Python and Ruby, andusakey_client_start_trial_verification/usakey_client_confirm_trial_verificationin 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(licenseIdand 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(reasonnotAcceptingordailyLimit)rateLimited(waitretryAfterseconds)
In Rust these are
TrialVerificationOutcome::AlreadyUsedandUnavailable. 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.
- Send
POST /runtime/v1/activations/{activation_id}/sealed-file-keyswith 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. - 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
entitlementis JSONtruefor this license (resolved the same way as for the certificate). - Success is
200with signeddatacontainingactivation_id,file_id,header_sha256,enc, andwrapped_key.wrapped_keyis 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 indetails.entitlement),422 SEALED_FILE_INVALID,503 SEALED_FILE_KEY_UNAVAILABLE(retry later), and403 PLAN_ENTITLEMENT_REQUIRED(withdetails.entitlementsealed_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_pathcheck 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'sentitlementwhen 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.
| State | How to identify it | Behavior |
|---|---|---|
| Active | Valid signatures and bindings, license_status=active, and contract, certificate, and required concurrent-use lease are all valid | Enable protected features; check again at next_heartbeat_in |
| Suspended | A signed heartbeat (200) reports license_status=suspended; at first activation, 403 LICENSE_SUSPENDED | Disable 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. |
| Revoked | 403 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_DEACTIVATED | Disable 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. |
| Expired | The 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 deactivated | 410 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 rule | 403 ACTIVATION_RISK_BLOCKED / LICENSE_RISK_BLOCKED | Disable and ask the user to contact the seller. Do not retry automatically. |
| Seller's Usakey subscription inactive | 402 BILLING_REQUIRED | Disable 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 limit | 403 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 key | 404 LICENSE_NOT_FOUND | Show that the key is invalid and ask for re-entry; do not try other keys automatically |
| Device-key mismatch | 409 DEVICE_KEY_MISMATCH | Show 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 limit | 409 MACHINE_LIMIT_REACHED | Show 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 assignment | 401 NAMED_USER_TOKEN_REQUIRED / NAMED_USER_TOKEN_INVALID, 409 NAMED_USER_MISMATCH, 403 NAMED_USER_ASSIGNMENT_UNAVAILABLE | Disable 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 code | 401 EMAIL_VERIFICATION_CODE_INVALID (details.remaining_attempts), 410 EMAIL_VERIFICATION_EXPIRED | This 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 authentication | 401 TWO_FACTOR_REQUIRED / TWO_FACTOR_INVALID (details.remaining_attempts), 403 TWO_FACTOR_LOCKED | For 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. |
| Trial | 403 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_EXCEEDED | Disable 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 limit | 409 CONCURRENCY_LIMIT_REACHED | Do not enable the protected feature for this launch; retry later without taking an existing user's concurrent-use lease. |
| Lease expired | 410 LEASE_EXPIRED or local expires_at reached | Always disable floating use and recover only after acquiring a new valid lease. |
| License unavailable during a lease operation | 403 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 license | 422 OFFLINE_PACKAGE_REQUIRED / LEASE_NOT_SUPPORTED / OFFLINE_UNMANAGED_HEARTBEAT_DISABLED | Record 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 required | 422 MACHINE_INFO_REQUIRED / MACHINE_INFO_REFRESH_REQUIRED | Resend with the full machine_info (the official SDK does this automatically). Do not show it to the user. |
| Clock skew or replayed request | 401 CLOCK_SKEW / NONCE_REPLAYED | Not 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 allowed | 403 BROWSER_ORIGIN_NOT_ALLOWED | The 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 mismatch | 401 DEVICE_PROOF_INVALID / ACTIVATION_MISMATCH / INSTANCE_MISMATCH | Disable and check the saved keys and activation ID. If that does not fix it, ask the user to activate again. |
| Malformed request | 400 INVALID_REQUEST / 413 REQUEST_TOO_LARGE / 422 CERTIFICATE_PAYLOAD_TOO_LARGE | Record it as an implementation error and fix it. Do not say the license was suspended. |
| Nothing to release or deactivate | 404 NOT_FOUND | When releasing a concurrent-use lease or deactivating the device, treat it as done. |
| Sealed files | 403 SEALED_FILE_ENTITLEMENT_REQUIRED / 422 SEALED_FILE_INVALID / 503 SEALED_FILE_KEY_UNAVAILABLE | None 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 429 | No 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_UNAVAILABLE | Use 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 mismatch | Response or certificate verification failed | Do not apply grace; disable and record a security error. |
Local decision during network failure
- First verify the cached certificate signature, product ID, device binding, and local rollback/tamper state.
- If
license_statusis not active orlicense_expiryhas passed, always disable. - Before certificate
expires_at, use is allowed. After it,fail_mode=closeddisables,gracecontinues with a warning untilexpires_at + network_grace_sec, andopencontinues with a degraded indicator. - 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
- 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.
- 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 ansk_key or the backend credential. - 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. - 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.refundedstripe 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.refundedKeep 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.
- Before opening checkout, generate and securely store a claim verifier and Idempotency-Key.
- Attach only the verifier hash to Checkout; never send the verifier itself to Stripe or the seller.
- 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.
- 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
- Generate 32 bytes from a cryptographically secure random source, encode them as 43 unpadded Base64URL characters, and store the
claim_verifierlocally before purchase. Generate a separate claimIdempotency-Key; persist the challenge, verifier, and Session ID when available unchanged until ACK. - Compute
base64url(SHA-256(UTF-8 bytes of claim_verifier))and prefix it withusk_claim_v1_. The hash is not secret, but never put the verifier itself in Stripe, the seller backend, or a URL. - 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. - Put
?session_id={CHECKOUT_SESSION_ID}in the backendsuccess_url, or in the Payment Link “After payment” redirect URL set in Stripe Dashboard (after_completion.redirect.urlin 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
| Response | Meaning | Product-app behavior |
|---|---|---|
| 201 | Key issued; waiting for ACK | Persist it safely, then ACK with the same Idempotency-Key. If the response was lost, retrieve with the same key. |
| 204 | ACK accepted | Temporary data is deleted; continue to normal device activation with the saved key. |
| 400 INVALID_REQUEST | Neither 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_FOUND | Wrong 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 key | For claim, retry with the same values during the window below. For ACK, confirm the original key. |
| 413 REQUEST_TOO_LARGE | The request body is too large (up to 1 MiB) | Fix the integration. |
| 429 RATE_LIMITED | Too many requests from one address (30 per minute) | Wait Retry-After seconds, then retry with the same values. Do not stop retrying. |
| 410 ALREADY_CLAIMED | Already ACKed or claimed with another key | Use the saved key; if it was not saved, contact the seller. |
| 410 NOT_REQUIRED | No new key was issued | Not an error; see trial conversion below. |
| 410 REVOKED | The license is revoked (cancellation, full refund, or revocation by the seller) | Do not retrieve a key; stop automatic retries for this purchase. |
| 410 EXPIRED | Claim 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.
- Store the
license_idreturned when issuing the trial and bind it to the purchaser in the seller backend. Use thelic_...ID, not the key. - When purchase starts, create Checkout with the saved ID in
metadata[usakey_license_id]and the app challenge inmetadata[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. - Put
?session_id={CHECKOUT_SESSION_ID}insuccess_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. - 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 (
offlinefeature); creates the signed request content. - export_offline_request
- The same function for each language's ; see that language's README for naming.
- 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.
- 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. - Target device: Carry the
.usakeylicon 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_UNAVAILABLEin 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.
- Use the embedded initial trust anchor to verify the signature and expiry of the public-key list (API name:
Trust Bundle). - Use the product long-term public key identified by
parent_key_idto verifysigning_delegation, then check product ID, purpose,online_key_id, and six-to-24-hour validity. - Confirm that
response_assertionmatches the product, parent key, short-lived key, nonce, and HTTP status. - Canonicalize the decision, compute SHA-256, and compare it with
body_sha256. - Verify
response_signaturewith 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, andnullfor 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
.usakeyreqsigned request file created by a fully offline device; it contains no license key or private key. - Entitlement file
- The
.usakeylicsigned 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.