CheckMyLicense LogoCheckMyLicense

Documentation

Learn how to use CheckMyLicense with step-by-step guides, API reference, and SDK examples.

User Guide

Get started, understand operations, and configure your workspace.

API Reference

Authenticate, create and manage licenses, and verify activations.

SDKs

Explore MQL5 and Python SDKs with installation, quick start, and examples.

User Guide

Overview

After logging in, the left‑side navigation divides the site into four main areas:

  • Operations – programs and licenses.
  • E‑Commerce – products, referees, customers, orders and payments.
  • Settings – user profile, organization, billing and balance.

Search boxes and "Add" buttons appear at the top of most pages, making it easy to find items and create new ones.

Important: The menu structure and available features may differ from user to user based on their assigned role (Administrator or Member) and their active subscription package. Administrators have full access to all features, while Members may have restricted access to certain administrative functions.

Operations

Dashboard

The dashboard summarises your organisation's usage. It displays counts of programs, total and active licences, and API hits, along with charts for licence creation and usage. Use it for a quick health check.

Programs

Purpose: Register software programs and their versions so that products and licences can reference them.

  1. Go to Programs and click Add Program.
  2. Enter a Program Name and Program Code (unique short identifier). Optionally upload a logo and write a description.
  3. Optionally add Configuration parameters (key-value pairs) that will be included in license verification responses.
  4. Click Create. The program appears in the table with actions to view (eye), edit (pencil) or delete (trash). The delete icon removes the program permanently.
  5. To add a version, click the eye icon, then Add Version. Provide a version number, description and upload the program file. Versions appear beneath the program with actions to edit or delete.

Program Configuration

The Configuration field allows you to define custom parameters that are returned during license verification. These parameters become part of the cryptographic signature, ensuring they cannot be altered by clients. This provides a secure way to control program behavior and enforce policies directly from your license server.

Use Cases for Configuration:

  • Minimum Version Enforcement: Set a min_version parameter (e.g., "2.5.0"). Your program can check this value and refuse to execute if the user's version is older, ensuring all users run up-to-date software.
  • Feature Unlocking: Define parameters like min_trades_for_feature to require users to complete a certain number of actions before unlocking premium features.
  • Runtime Limits: Control parameters like max_accounts, max_symbols, or session_timeout to enforce usage limits.
  • Feature Flags: Enable or disable specific features using boolean flags like allow_backtesting or enable_advanced_mode.

Security Note: Configuration values are included in the HMAC signature during license verification, making them tamper-proof. Clients receive these values as part of the signed response and cannot modify them without invalidating the signature. This ensures your program logic can trust these parameters completely.

Release Management: A complete release cycle is available through program versions. When you publish a new version, users with active licenses can receive automated email notifications about the update, ensuring they stay informed about the latest releases of your software.

Licences

Purpose: Generate activation keys that allow customers to use your program.

  1. Navigate to Licences and click Add Licence.
  2. Select the Program, type the Customer Email, set a Limit (devices allowed, 0 = unlimited) and an Expiry Date (or leave blank for unlimited).
  3. Click Generate. A unique licence key appears; copy it or send via email. The licence list shows email, program and status (Active, Expired, etc.), with actions to view or edit a licence.

License Management

After creation, only the license status can be edited. Each license maintains its own list of activations, which can be managed based on the Activations Deletable flag. When enabled, users can delete individual activations if needed.

End users can manage their activations directly at www.checkmylicense.dev/myactivations, or you can integrate the activation management flow directly into your own website using the My Activations API together with a Turnstile Widget.

Turnstile Widgets

Turnstile widgets are used to protect the activation management API when you embed it into your own website. Each widget is backed by a Cloudflare Turnstile key pair and is registered in CheckMyLicense under your Organisation settings.

You will need two values from this setup: the widget's Public Key (used as widget_key in API calls) and your Organisation ID (displayed in the Turnstile Widgets section of the Organisation page and used as organization_id).

Step 1 — Create a Turnstile widget in Cloudflare

  1. Log in to the Cloudflare dashboard and navigate to Turnstile in the left sidebar.
  2. Click Add widget, give it a name, and enter the domain(s) where it will be used.
  3. Choose a widget type (Managed is recommended) and click Create.
  4. Copy the Site Key (public) and the Secret Key — you will need both in the next step.

Step 2 — Register the widget in CheckMyLicense

  1. Go to Organisation (under Settings in the left navigation).
  2. Scroll down to the Turnstile Widgets section.
  3. Click Add Widget and fill in:
    • Description — a label for your own reference (e.g. “My Store Widget”).
    • Public Key — the Site Key you copied from Cloudflare.
    • Secret Key — the Secret Key you copied from Cloudflare (stored encrypted).
    • Status — set to Active.
  4. Click Save. The widget now appears in the list with a truncated Widget ID and its Public Key.

Step 3 — Note your Organisation ID

An information bar at the top of the Turnstile Widgets section shows your Organisation ID. Keep this value alongside your widget's Public Key — both are required when calling the My Activations API from your website.

API Integration

CheckMyLicense provides a ready-to-use API for license operations that you can integrate directly with your programs:

  • License Creation – programmatically generate licenses
  • License Update – modify license status and parameters
  • License Check – verify license validity and create activations

For detailed API documentation, parameters, and code examples, please refer to the API section of this help guide.

E‑Commerce

This section enables you to sell programs, reward affiliates, manage customers and handle orders and payments.

API Available: A comprehensive E-Commerce API is now available, allowing you to integrate CheckMyLicense with your website as a full-fledged backend solution. This enables seamless product sales, order management, and payment processing directly through your own website/s. Check the API section for details.

Products

Products allow you to commercialize your programs in different packages, offering flexibility in how you monetize your software. Each product can have its own configuration including different activation limits, validity periods, and pricing structures. This opens a wide range of possibilities for your commercial imagination—from offering trial versions with limited activations to premium packages with extended validity and unlimited devices.

Create a Product

  1. Navigate to Products and select Add Product.
  2. Specify a Title and Product Code, select the Program it belongs to and set the Currency and Price.
  3. Mark Discountable if referral codes should apply discounts.
  4. Define Limit (number of licences per purchase, 0 = unlimited) and Validity (days before licence expiry, 0 = no expiry).
  5. Write a description for storefront display.
  6. Click Create. The product appears with actions to view, edit or delete.

Referees and Referral Codes

Referees are affiliates who refer customers using referral codes.

Add Referee

  1. Navigate to Referees and click Add Referee.
  2. Provide the contact's email, first name and last name.
  3. Enter phone number, country, state and address.
  4. Click Save.
  5. The referee appears in the list with actions to view or edit.

Note: There is no delete option; a referee can only be cancelled (deactivated).

Roadmap: Referee compensation is not currently handled by CheckMyLicense, but it is on our roadmap. We welcome your ideas and feedback on how you would like this feature to work. Please don't hesitate to share your suggestions.

Create Referral Code

Open a referee's detail page and click Add Referral. Enter a unique code and select whether the discount is Percentage Based or Fixed Amount. Provide the discount value, optionally select a Program to limit its scope, and set an Expiry Date (or leave blank for "Never expires").

If Allow Multiple Usage is checked, the code can be redeemed unlimited times; otherwise the usage limit is exactly one. You cannot set custom usage counts beyond one or unlimited.

Customers

To create an order, you must have a customer on file.

  1. Go to Customers and click Add Customer.
  2. Enter the Email, First Name, Last Name, Phone, Country, State and Address. Click Create.
  3. The customer list shows names and emails with actions to view, edit or delete.

Note: Customers can be deleted as long as they don't have any orders associated with them. Once a customer has orders, they can only be deactivated if necessary.

Orders

Order Statuses

  • Draft – created but not confirmed. Can be edited or cancelled. Payment cannot be recorded at this stage.
  • Pending Payment – order confirmed and awaiting payment. At this status, you may record payments. Once the paid amount equals the order total the status becomes Paid; partial payments change the status to Partial Paid.
  • Paid / Partial Paid – payment recorded. A backend job (runs hourly) automatically generates the associated licences. Paid orders can be cancelled within 24 hours; the status then changes to Refunded and another backend job deactivates the licences.
  • Cancelled – order cancelled before payment; no licences generated.

Create an Order

  1. Navigate to Orders and click Create Order. Select a Customer. Optionally choose a Referral Code; the discount value is calculated automatically.
  2. Click Add Product, select the desired product and set the quantity. You may add multiple products. The order total and discount update in real time.
  3. Click Create Order. A confirmation modal shows the order code and total. Confirming sets the status to Draft. Open the order details to review.
  4. On the order details page, use the icons to Edit (pencil), Confirm (blue check) or Cancel (red icon). Confirming moves the order to Pending Payment; cancelling at this stage removes the order. There is no deletion; cancellation simply marks the order as cancelled.

Payments

Payments must correspond to orders that are Pending Payment (not Draft or already Paid). CheckMyLicense provides a payment screen to handle transactions directly through the platform.

Record a Payment

  1. Go to Payments and click Add Payment.
  2. Fill in the following details:
    • Order Code – e.g., ORD251017133003DR1
    • Amount – amount paid
    • Provider – name of the payment provider (e.g., cash, stripe, paypal)
    • Status – choose from Captured (successful), Failed, etc.
    • Payment Method and Provider Transaction ID – optional reference values
  3. Click Record Payment. If the order status is Pending Payment, the payment is accepted and appears in the payments list. If the order is still Draft or already Paid, the system rejects the payment.

Payments update the Paid Amount field in the order. When total paid equals the order total, the order status changes to Paid. Licences are then generated automatically by the backend job.

Roadmap: A webhook will be available to update payment information directly from your website or platform. Integrations with Xpay, PayPal, and Stripe are planned for future releases, enabling seamless payment processing.

Settings

User Profile

Use User Profile to manage your account:

  • Upload or update your Profile Picture.
  • Edit your Name, Email and Phone Number.
  • Change your password via the Change Password dialog after entering your current password.

For Members: Users who are members of an organization (not administrators) have the option to leave the organization. However, leaving will result in loss of access to the organization's resources unless you are re-invited by an administrator.

Organization

This section contains organisation‑wide settings.

Administrator Role

Each organization has one administrator who has exclusive access to critical functions including:

  • Managing billing and subscription plans
  • Inviting and managing team members
  • Accessing API integration keys
  • Modifying organization-wide settings

Members are added to the organization via email invitation. The administrator can delegate their role to another team member through the Change Admin function, but doing so will transfer all administrative privileges to the new administrator and revoke them from the current one.

Organization Settings

  1. Organization Information: Edit fields such as organisation name, type (Personal or Company), phone, email, country, website, Tax ID and billing address.
  2. Email & SMTP Settings: Configure custom email settings to send transactional emails (license deliveries, notifications) from your own domain. If you don't have an SMTP server or prefer not to configure one, the platform will automatically use the built-in email service (checkmylicense@tradet.net) to send all emails on your behalf. This ensures your organization can function seamlessly without requiring SMTP configuration. When custom SMTP settings are configured, you can:
    • Set up SMTP Server – provide your SMTP host, port, username, and password for authentication.
    • Customize Email Sender – define the "from" email address and support email for your organization.
    • Test Configuration – send a test email to verify your SMTP settings are working correctly.
    • Update or Delete – modify your settings at any time or remove them to revert to the default platform email service.

    Note: Email settings are optional. Without custom SMTP configuration, all transactional emails will be sent from checkmylicense@tradet.net. Configure SMTP only if you need emails to come from your own domain for branding purposes.

  3. API Keys: Manage your API authentication credentials for license verification operations. You can create up to 3 API keys, each consisting of a unique key (UUID) and secret that are auto-generated for security. Each API key can be:
    • Created – click Create API Key and provide a description (e.g., "Production API", "Development API").
    • Activated/Deactivated – edit the status to control which keys are currently valid for authentication.
    • Deleted – permanently remove keys you no longer need.
    • Copied – use the copy buttons to quickly copy the key or secret to your clipboard.
    • Hidden/Shown – toggle visibility of the key and secret values for security.
    Keep your API keys secure and do not share them publicly. Only active keys will authenticate successfully.
  4. Members: Click Manage Members to view and manage your team. Each member entry shows name, email, role (ADMIN or MEMBER), status (Active or Suspended), invited and joined dates. Actions allow:
    • Invite Member – enter an email address to send an invitation link.
    • Change Admin – assign the admin role to another member.
    • Suspend – temporarily remove access from a member. Suspended members can be reactivated later.

There are no limits on the number of members you can invite at present.

Billing

The billing page displays your current consumption and estimated overage costs. You see your next billing date and the usage limits for programs, active licences and API hits. Exceeding these limits incurs overage charges depending on your subscription plan. A list of invoices appears once your organisation has billable transactions.

Subscription Plans

CheckMyLicense offers several subscription packages, each designed to meet different business needs. Plans include varying limits for programs, active licenses, and API hits, with options for overage support when you exceed included limits.

Available Plans

  • Starter Kit (Free) – Perfect for getting started with basic features and community support.
  • Pay as you Go – Flexible usage-based pricing with email support.
  • Growth – Includes E‑Commerce support, email support, and overage capability for growing businesses.
  • Pro – Enterprise-grade features with multi‑user support, priority support, and white‑labelled emails.

You can view your current subscription via Billing → Manage Subscription, which shows your active plan, billing period and included features. Use Cancel Subscription to stop billing, or upgrade to higher-tier packages as your business grows.

Balance and Add Credit

The Balance page shows your available credit (for overage fees) and a ledger of credit transactions.

Add Credit

  1. Click Add Credit. Enter the amount and select a payment method:
    • Xpay (USDT TRC20) – redirects to the Xpay portal where you can complete the cryptocurrency payment.
    • PayPal – redirects to PayPal's SDK for processing.
    • Other Payment Method – opens a support form where you may arrange offline payment (e.g., bank transfer, cheque deposit). The operations team manually adds the credit after verifying the payment.
  2. Check the box agreeing that credits will pay current and future dues.
  3. Follow the external instructions to finish the payment. On returning, your balance will update.

Typical Workflow

Use the following steps to sell a program to a customer:

  1. Create a Program and add a version under Operations → Programs.
  2. Create a Product linked to that program under E‑Commerce → Products.
  3. (Optional) Add a Referee and Referral Code if you wish to offer discounts.
  4. Add a Customer under E‑Commerce → Customers.
  5. Create an Order for the customer in Orders. Select the product, apply any referral code and confirm the order. The order enters Draft status.
  6. Confirm the Order via the blue check icon on the order page. The status becomes Pending Payment.
  7. Record Payment in Payments. Provide the order code, amount and provider details. Once payments cover the total, the order status becomes Paid and licences are generated automatically by the backend job (runs hourly).
  8. Monitor Licences and Overage Usage in Operations → Licences and Settings → Billing.

Orders can be cancelled before payment or within 24 hours after payment (status changes to Refunded). There is no delete function; cancelling is the only way to void an order or referral.

Frequently Asked Questions

Order and Payment Workflow

Payments are only accepted when an order is in Pending Payment status. Attempting to add a payment while the order is still Draft or already Paid results in "Failed to insert payment."

Cancellations

There is no delete function for orders, programmes, products or referrals. You can cancel orders (Draft or Pending Payment) or refund paid orders within 24 hours. Licences are created or deactivated automatically by backend jobs.

Referral Code Usage

If Allow Multiple Usage is unchecked, a referral code may be used once. There is no way to specify a custom usage count. When checked, the code can be used unlimited times.

Add Credit Methods

Xpay and PayPal redirect you to their respective payment portals, while Other Payment Method contacts support for manual payment arrangements (e.g., bank transfer or cheque deposit).

Multi‑User Limits

There are no limits on the number of members you can invite to your organisation.

API Documentation

Authentication

Authenticate with email/password to receive a session object. Use the returned accessToken as a Bearer token in Authorization header for protected endpoints.

Sign In with Email/Password

POST/signin/email-password

Request body: email and password.

ParameterTypeInRequiredDescription
emailstringbodyyesUser email
passwordstringbodyyesUser password
Example Request
POST https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/signin/email-password HTTP/1.1
Content-Type: application/json

{
  "email": "john.doe@example.com",
  "password": "MyP@ssw0rd!"
}
Example Response
{
  "session": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjA2MjEwMjMs...",
    "accessTokenExpiresIn": 900,
    "refreshToken": "b62f696a-b61b-4f2e-9e74-f4f18c6062d3",
    "refreshTokenId": "9fe23c69-da50-4212-b467-0a219eb9b4b1",
    "user": {
      "id": "693de173-d234-509a-a1dc-4d40da908866",
      "email": "john.doe@example.com",
      "displayName": "John Doe"
    }
  }
}

Use the access token in subsequent requests:

Authorization: Bearer <accessToken>

Create License

Create License

POST/license/create

Create a new license. Implemented in functions/license/create. The endpoint requires organization context (auth provides organization_id).

ParameterTypeInRequiredDescription
emailstringbody or queryyesLicense owner email
program_namestringbody or queryyesProgram name
limitintegerbody or querynoActivation limit
expirybigintbody or querynoExpiry timestamp (seconds or ms accepted)
activations_deletablebooleanbody or querynoWhether activations can be deleted
cURL Example
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/create" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "program_name": "My Product",
    "limit": 3,
    "expiry": 1761315325262
  }'

Possible Responses

200License created

Success

{
  "success": {
    "message": "License created",
    "data": {
      "insert_licenses": {
        "affected_rows": 1,
        "returning": [{
          "id": 309,
          "email": "example@tradet.net",
          "license": "9845cca98bbe4664bbbc9fb10862a453",
          "program_id": 42,
          "limit": 10,
          "expiry": null,
          "status": 0,
          "activations_deletable": true,
          "organization_id": "723493dd-4a3c-cd4d-8b7b-c288704d96e9",
          "created_at": "2025-10-17T09:08:59.838129+00:00",
          "updated_at": "2025-10-17T09:08:59.838129+00:00"
        }]
      },
      "subscription_info": {
        "subscription_id": 70,
        "package_id": 36,
        "overage_created": false,
        "is_vip": false,
        "current_license_count": 5,
        "license_limit": 10,
        "reason": "Within package limits"
      }
    }
  }
}
400Invalid request / validation failed

Missing or invalid parameters

{
  "error": {
    "message": "Invalid license request",
    "errors": ["email is required", "program_name not found"]
  }
}
400Missing organization

Caller is not associated with an organization

{
  "error": {
    "message": "User must belong to an organization to create licenses"
  }
}
400Program validation failed

Program not owned by organization or not found

{
  "error": {
    "message": "Program not found or not owned by this organization"
  }
}
400Subscription limit / quota prevented creation

Subscription does not allow creating more licenses

{
  "error": {
    "message": "Subscription limits prevent creating new licenses",
    "reason": "license limit reached for current package"
  }
}
500Insert error

General failure while processing the request

{
  "error": {
    "message": "Error creating license",
    "data": { "message": "<error object>", "stack": "..." }
  }
}

Update License

Update License

PUT/license/update

Update license fields such as status. Implemented in functions/license/update. The handler expects license_id and any fields to set.

ParameterTypeInRequiredDescription
license_idintegerquery or bodyyesLicense ID to update
statusintegerquery or bodyno0=active, 1=inactive (will set deactivated_at)
cURL Example
curl -X PUT "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/update" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "license_id": 124, "status": 1 }'
Example Response
{
  "success": {
    "message": "License updated: status and deactivated_at updated",
    "data": {
      "update_licenses": {
        "affected_rows": 1,
        "returning": [{
          "id": 309,
          "status": 1,
          "deactivated_at": "2025-10-17T09:23:23.35+00:00",
          "updated_at": "2025-10-17T09:23:23.38114+00:00"
        }]
      }
    }
  }
}

Verify License

Verify License

POST/license/verify

Verify a license and create an activation record using HMAC-SHA256 signature authentication. Implemented in functions/license/verify.

ParameterTypeInRequiredDescription
api_idstring (UUID)bodyyesOrganization ID (from Settings → Organization)
tsstringbodyyesCurrent Unix timestamp in seconds (UTC)
payloadstring (base64)bodyyesBase64-encoded JSON payload with license, email, activation details, and program name. The server will verify the provided program matches the license's program and will return 400 invalid program on mismatch.
sigstring (base64)bodyyesHMAC-SHA256 signature of canonical string (base64 encoded)
vstringbodynoAPI version (default: "3"). Versions "1" and "2" are legacy and no longer supported.
cURL Example
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/verify" \
  -H "Content-Type: application/json" \
  -d '{
    "api_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "ts": "1729875123",
    "payload": "BASE64_ENCODED_PAYLOAD",
    "sig": "BASE64_HMAC_SIGNATURE",
    "v": "3"
  }'

Payload Structure (JSON, before base64 encoding)

License verification payload
{
  "license": "9845cca98bbe4664bbbc9fb10862a453",
  "email": "john@example.com",
  "target_id": "123456789",
  "target_host": "MT5-PC\\Server",
  "type": "Live",
  "program": "My Product"
}

Field Descriptions:

  • license: The full 32-char hex license key
  • email: License owner's email address (must match the license, comparison is case-insensitive)
  • target_id: Account number or unique machine identifier
  • target_host: Server name or hostname
  • type: Account type (e.g., "Live", "Demo")
  • program: Program name (must match the license's program)

Limits and Expiry:

  • limit === 0: Means unlimited activations. The server checks prospective usage as used + 1 against the limit.
  • expiry: The license expiry in the system may be stored as seconds or milliseconds. The server handles both; response validity will match the stored value. Clients should treat validity as a timestamp (ms or large number).

Possible Responses

StatusMessageDescription
200activation existsLicense verified successfully. This target already has an active license.
200activation createdFirst time this target is checking this license. New activation was created.
400missing required fieldsOne or more required fields (api_id, ts, payload, sig) are missing or empty.
401stale timestampTimestamp is too old or too far in the future (must be within ±60 seconds of server time).
401invalid api_idAPI Key (api_id) not found or inactive in the system.
401invalid signatureHMAC-SHA256 signature verification failed. Verify your api_secret and signature computation.
400invalid payloadPayload cannot be decoded from base64 or is not valid JSON.
404unmatched licenseNo license matching the provided UUID exists for this organization.
400license inactiveLicense status is 1 (inactive/deactivated). Cannot use inactive licenses.
400invalid emailProvided email does not match the license owner email.
400expired certificateLicense expiry timestamp has passed. License has expired.
400max limit reachedMaximum number of activations for this license has been exceeded.
500internal errorInternal server error while processing the verification request.

Activation Tracking

  • An organization-level 'hit' is created immediately after org verification. If the license is found, the hit is updated with license_id and the license's last_used_at is updated.
  • Activation insertion is best-effort; verification succeeds even if activation DB insert fails.
  • If an activation with matching target_id, target_host, and type already exists, the server returns 200 with message: 'activation exists'.

Encoding & Encryption

The license verify endpoint uses HMAC-SHA256 signature authentication. The request body is JSON, but the payload field must be base64-encoded and the request is authenticated with an HMAC-SHA256 signature.

Request Signature Flow

To verify a license, follow these steps:

  1. Prepare JSON payload with license, email, and target info
  2. Base64 encode the JSON payload
  3. Get current Unix timestamp (seconds, UTC)
  4. Build canonical string exactly as: api_id.ts.payload_base64 — where payload_base64 is the literal base64 string you include in the payload field. Do NOT decode or alter the base64 string when creating the canonical string.
  5. Compute HMAC-SHA256 of canonical string using your api_secret
  6. Base64 encode the HMAC result
  7. Send JSON request with api_id, ts, payload, sig, and v fields

Building the Payload

The JSON payload contains the license and activation information:

Example payload (before base64 encoding)
{
  "license": "9845cca98bbe4664bbbc9fb10862a453",
  "email": "john@example.com",
  "target_id": "123456789",
  "target_host": "MT5-Desktop\\EURUSD",
  "type": "Live",
  "program": "My Product"
}

Payload Fields:

  • license: 32-character hex license key
  • email: Email address of the license owner
  • target_id: Account login number or unique machine ID
  • target_host: Server hostname or terminal identifier
  • type: Account type (e.g., "Demo", "Live")
  • program: Program name (must match the license's program)
Example payload (base64 encoded)
eyJsaWNlbnNlIjoiOTg0NWNjYTk4YmJlNDY2NGJiYmM5ZmIxMDg2MmE0NTMiLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJ0YXJnZXRfaWQiOiIxMjM0NTY3ODkiLCJ0YXJnZXRfaG9zdCI6Ik1UNS1EZXNrdG9wXFxFVVJVU0QiLCJ0eXBlIjoiTGl2ZSIsInByb2dyYW0iOiJNeSBQcm9kdWN0In0=
Canonical String Example
Input:  api_id = "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
        ts = "1729875123"
        payload_base64 = "eyJsaWNlbnNlIjoiOTg0NWNjYTk4YmJlNDY2NGJiYmM5ZmIxMDg2MmE0NTMiLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJ0YXJnZXRfaWQiOiIxMjM0NTY3ODkiLCJ0YXJnZXRfaG9zdCI6Ik1UNS1EZXNrdG9wXFxcXEVVUlVTRCIsInR5cGUiOiJMaXZlIiwicHJvZ3JhbSI6Ik15IFByb2R1Y3QifQ=="

Output: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d.1729875123.eyJsaWNlbnNlIjoiOTg0NWNjYTk4YmJlNDY2NGJiYmM5ZmIxMDg2MmE0NTMiLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJ0YXJnZXRfaWQiOiIxMjM0NTY3ODkiLCJ0YXJnZXRfaG9zdCI6Ik1UNS1EZXNrdG9wXFxcXEVVUlVTRCIsInR5cGUiOiJMaXZlIiwicHJvZ3JhbSI6Ik15IFByb2R1Y3QifQ=="

Computing HMAC-SHA256 Signature

Use your api_secret (from Settings → Organization) and the canonical string to compute an HMAC-SHA256 digest, then base64-encode it.

Steps:

  1. Take the canonical string: api_id.ts.payload_base64
  2. Compute HMAC-SHA256 using your api_secret as the key and the canonical string as the data
  3. Base64 encode the resulting HMAC digest
  4. Use this base64-encoded value as the sig parameter

Most modern programming languages have built-in libraries for HMAC-SHA256 computation. Look for crypto, hashlib, or similar security libraries in your language of choice.

Timestamp Window: Default ±60 seconds (server uses env LICENSE_VERIFY_TS_WINDOW to override). Ensure your system clock is synchronized.

API Secret Lookup: Server fetches the organization secret from the database (Settings → Organization). For local/testing, a fallback API_SECRETS_JSON env var can provide {"apiId": "secret"} mappings — internal/testing only.

Response Signing

On successful responses, the server adds server_sig and server_ts. The server computes server_sig as base64(HMAC-SHA256) over the canonical response string:

Canonical Response String Format
${api_id}.${server_ts}.${success}.${message}.${base64(JSON.stringify(data))}

If data is empty, the last segment is an empty string. server_ts is the server unix time in seconds (string). Clients can verify server_sig with the organization's api_secret.

Example Canonical Response String
Input:  api_id = "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
        server_ts = "1729875130"
        success = "true"
        message = "activation exists"
        data = {"validity":1761315325262,"limit":3,"used":1}

Output: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d.1729875130.true.activation exists.eyJ2YWxpZGl0eSI6MTc2MTMxNTMyNTI2MiwibGltaXQiOjMsInVzZWQiOjF9"

Complete Request Example

Full JSON request body
{
  "api_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "ts": "1729875123",
  "payload": "eyJsaWNlbnNlIjoiOTg0NWNjYTk4YmJlNDY2NGJiYmM5ZmIxMDg2MmE0NTMiLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJ0YXJnZXRfaWQiOiIxMjM0NTY3ODkiLCJ0YXJnZXRfaG9zdCI6Ik1UNS1EZXNrdG9wXFxcXEVVUlVTRCIsInR5cGUiOiJMaXZlIiwicHJvZ3JhbSI6Ik15IFByb2R1Y3QifQ==",
  "sig": "BASE64_HMAC_SIGNATURE",
  "v": "3"
}

Breaking it down:

  • api_id = Organization ID
  • ts = Current Unix timestamp (seconds)
  • payload = Base64(JSON payload with program field)
  • sig = Base64(HMAC-SHA256(api_id.ts.payload, api_secret))
  • v = API version (default: "3", versions "1" and "2" are legacy)

Response Handling

The response is always JSON. Check the HTTP status code:

  • 200 OK — License verified. Response contains success: true, message, and data with validity/limit/used.
  • 400-500 — Error. Response contains error object with code and message.
Success response (200 OK)
{
  "success": true,
  "message": "activation exists",
  "data": {
    "validity": 1761315325262,
    "limit": 3,
    "used": 1,
    "program_configuration": {
      "min_version": "2.5.0",
      "min_trades_for_feature": "50",
      "max_accounts": "3"
    },
    "offline_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",  // optional - best-effort
    "offline_expires_at": 1761910123  // unix seconds
  },
  "server_sig": "BASE64_HMAC_SIGNATURE",
  "server_ts": "1729875130"
}

Response Data Fields:

  • validity: License expiry timestamp (may be in seconds or milliseconds)
  • limit: Maximum number of activations allowed (0 = unlimited)
  • used: Current number of activations
  • program_configuration: Key-value pairs defined in the program settings (optional — only present if configured). These values are part of the cryptographic signature and cannot be tampered with.
  • offline_token: Base64 JSON token containing validity, limit, used, and configuration fields (optional — generated best-effort). When decoded, the offline token includes a configuration field (not program_configuration) with the same values.
  • offline_expires_at: Unix seconds expiry for the offline token (optional)

Note: Clients should accept responses that omit offline_token or program_configuration. The offline token uses the key name configuration while the main response uses program_configuration.

Error response (e.g., 401 Unauthorized)
{
  "error": {
    "code": 401,
    "message": "invalid signature"
  }
}

Signature Verification Failed?

  • Verify your api_secret is correct
  • Ensure timestamp is recent (within ±60 seconds of server time, configurable via LICENSE_VERIFY_TS_WINDOW)
  • Confirm canonical string format: api_id.ts.payload_base64 (with periods, no spaces)
  • Use the literal base64 string from the payload field — do NOT decode or alter it when building the canonical string
  • Check that HMAC output is base64-encoded for the sig field

Create Customer

Create Customer

POST/order/customer/create

Creates a new customer record within the organization. Validates email format and uniqueness, phone number format (if provided), and enforces required fields. Customers are scoped to the authenticated user's organization.

ParameterTypeInRequiredDescription
customer.emailstringbodyyesCustomer email address. Must be valid format and unique within organization.
customer.first_namestringbodyyesCustomer first name. Cannot be empty.
customer.last_namestringbodyyesCustomer last name. Cannot be empty.
customer.countrystringbodynoCountry name or code.
customer.statestringbodynoState, province, or region.
customer.addressstringbodynoFull street address.
customer.phonestringbodynoPhone number (international format supported). Must have at least 7 digits.
customer.refereebooleanbodynoWhether customer is a referee. Defaults to false.
Example Request
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/customer/create" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {
      "email": "john.doe@example.com",
      "first_name": "John",
      "last_name": "Doe",
      "country": "United States",
      "state": "California",
      "address": "123 Main Street, Apt 4B",
      "phone": "+1 (555) 123-4567",
      "referee": false
    }
  }'

Validation Rules

  • email: Must match regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  • first_name: Cannot be empty after trimming
  • last_name: Cannot be empty after trimming
  • phone: Must contain at least 7 digits; allows +, -, spaces, parentheses

Possible Responses

200Customer created successfully

Customer created and returned with ID

{
  "success": true,
  "data": {
    "customer": {
      "id": 42,
      "email": "john.doe@example.com",
      "first_name": "John",
      "last_name": "Doe",
      "country": "United States",
      "state": "California",
      "address": "123 Main Street, Apt 4B",
      "phone": "+1 (555) 123-4567",
      "referee": false,
      "organization_id": "6e8a2322-37de-4b91-8a0e-67ebd3873643",
      "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "created_at": "2025-12-29T12:00:00.000Z"
    }
  }
}
400Bad Request

Missing required fields, invalid email format, invalid phone format, or duplicate email

{
  "error": {
    "code": 400,
    "message": "Invalid email format"
  }
}
401Unauthorized

Missing or invalid JWT token

{
  "error": {
    "code": 401,
    "message": "Authentication required"
  }
}
403Forbidden

User lacks organization membership

{
  "error": {
    "code": 403,
    "message": "Organization membership required"
  }
}
409Conflict

Email already exists in organization

{
  "error": {
    "code": 409,
    "message": "Customer with this email already exists"
  }
}

Notes:

  • Email is case-sensitive and stored as-provided
  • Phone validation allows international formats
  • All string fields are automatically trimmed
  • user_id and organization_id are automatically set from JWT context

Submit Order

Submit Order

POST/order/submit

Creates a new order with line items, calculates pricing with optional referral discounts, and supports both single-use and multiple-use referral codes. Automatically consolidates duplicate product entries and validates product availability within the organization.

ParameterTypeInRequiredDescription
order.codestringbodynoCustom order code. Auto-generated if omitted.
order.customer_idintegerbodyyesID of the customer placing the order. Must belong to organization.
order.referral_codestringbodynoReferral/discount code to apply.
order.itemsarraybodyyesArray of line items with product_id and qty.
order.items[].product_idintegerbodyyesProduct ID. Must belong to organization.
order.items[].qtyintegerbodyyesQuantity (positive integer). Duplicates are consolidated.
Example Request
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/submit" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "order": {
      "code": "ORD2512290001ABC",
      "customer_id": 42,
      "referral_code": "WELCOME10",
      "items": [
        { "product_id": 15, "qty": 2 },
        { "product_id": 18, "qty": 1 }
      ]
    }
  }'

Warning Messages

The warnings array may include:

  • "Referral code not found" - Provided referral code doesn't exist
  • "Referral expired" - Referral code has passed expiry date (attached but no discount applied)
  • "Referral already used" - Single-use referral already redeemed (attached but no discount applied)
  • "Referral scoped but no programs linked" - Scoped referral has no eligible programs configured

Possible Responses

200Order created successfully

Order created with line items and optional discount applied

{
  "success": true,
  "warnings": [],
  "data": {
    "referral_applied": true,
    "order": {
      "id": 114,
      "code": "ORD2512290001ABC",
      "status": 0,
      "currency": "USD",
      "original_amount": 59.97,
      "discount_amount": 5.99,
      "final_amount": 53.98,
      "customer_id": 42,
      "referral_id": 7,
      "created_at": "2025-12-29T10:30:15.123Z",
      "order_items": [...]
    }
  }
}
400Bad Request

Invalid customer_id, missing items, invalid product prices, or duplicate order code

{
  "error": {
    "code": 400,
    "message": "Customer not found in organization"
  }
}
404Not Found

Customer or products not found in organization

{
  "error": {
    "code": 404,
    "message": "Product not found"
  }
}

Update Order

Update Order

POST/order/update

Updates an existing draft order (status = 0) with new line items and/or referral code. Recalculates pricing, replaces all items, and manages referral usage counters. Only the order owner can update their orders.

ParameterTypeInRequiredDescription
order.idintegerbodyyesID of the order to update. Must be in draft status (0).
order.codestringbodynoNew order code (optional). Must be unique within organization.
order.referral_codestringbodynoNew referral code. null removes existing referral.
order.itemsarraybodyyesComplete replacement item list. Previous items are deleted.
order.items[].product_idintegerbodyyesProduct ID. Must belong to organization.
order.items[].qtyintegerbodyyesQuantity (positive integer).
Example Request
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/update" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "order": {
      "id": 114,
      "code": "ORD2512290001XYZ",
      "referral_code": "SAVE20",
      "items": [
        { "product_id": 15, "qty": 5 },
        { "product_id": 20, "qty": 2 }
      ]
    }
  }'

Possible Responses

200Order updated successfully

Order updated with new items and/or referral

{
  "success": true,
  "warnings": [],
  "data": {
    "referral_applied": true,
    "order": {
      "id": 114,
      "code": "ORD2512290001XYZ",
      "status": 0,
      "original_amount": 124.93,
      "discount_amount": 24.99,
      "final_amount": 99.94,
      "referral_id": 12,
      "updated_at": "2025-12-29T11:45:22.456Z",
      "order_items": [...]
    }
  }
}
400Bad Request

Invalid order id, non-draft status, missing items, duplicate code, or invalid products

{
  "error": {
    "code": 400,
    "message": "Order is not in draft status"
  }
}
403Forbidden

Order not owned by current user

{
  "error": {
    "code": 403,
    "message": "Not authorized to update this order"
  }
}

Notes:

  • Previous referral usage is decremented if a discount was applied
  • New referral usage is incremented only if new discount is applied
  • All previous order_items and referral_redemptions are deleted and recreated
  • updated_at timestamp is automatically set

Confirm Order

Confirm Order

POST/order/confirm

Transitions a draft order (status = 0) to pending payment (status = 1). This is typically the first step after order submission before payment processing. Only draft orders can be confirmed.

ParameterTypeInRequiredDescription
order_idintegerbodyyesID of the order to confirm. Must be in draft status (0).
Example Request (Primary format)
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/confirm" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": 114
  }'
Alternative Request Format
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/confirm" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "order": {
      "id": 114
    }
  }'

Order Status Reference

StatusLabelDescription
0DraftOrder created but not confirmed
1Pending PaymentOrder confirmed, awaiting payment
2Partially PaidSome payment received
3PaidFully paid
4CancelledOrder cancelled
5RefundedPayment refunded
6Partially RefundedPartial refund issued

Possible Responses

200Order confirmed

Order status changed to pending payment (1)

{
  "success": true,
  "data": {
    "order": {
      "id": 114,
      "code": "ORD2512290001ABC",
      "status": 1,
      "updated_at": "2025-12-29T10:35:42.789Z"
    }
  }
}
400Bad Request

Invalid order_id or order not in draft status

{
  "error": {
    "code": 400,
    "message": "Order is not in draft status"
  }
}
404Not Found

Order not found in organization

{
  "error": {
    "code": 404,
    "message": "Order not found"
  }
}

Cancel Order

Cancel Order

POST/order/cancel

Cancels an order with optional refund processing and license deactivation. Supports flexible cancellation scenarios: simple status change, refund creation (full or partial), and asynchronous license deactivation via job queue. Idempotent for already-cancelled orders.

ParameterTypeInRequiredDescription
order_idintegerbodyyesID of the order to cancel.
refundobjectbodynoRefund configuration. Omit for no refund.
refund.enabledbooleanbodynoSet to true to create refund payment.
refund.amountnumberbodynoSpecific refund amount. Omit for full auto-calculated refund.
deactivate_licensesbooleanbodynoSet to true to queue license deactivation job.
notesstringbodynoCancellation notes (stored in order.notes).
Example Request (Simple Cancellation)
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/cancel" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": 114,
    "notes": "Customer requested cancellation"
  }'
Example Request (With Partial Refund and License Deactivation)
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/cancel" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": 114,
    "refund": {
      "enabled": true,
      "amount": 25.00
    },
    "deactivate_licenses": true,
    "notes": "Partial refund with license revocation"
  }'

Cancellation Scenarios

Scenariorefund.enableddeactivate_licensesBehavior
Simple Cancelfalse or omittedfalse or omittedSets status to 4, updates canceled_at
Cancel with Full Refundtrue, no amountfalse or omittedCancels + creates refund for all captured funds
Cancel with Partial Refundtrue, with amountfalse or omittedCancels + creates refund for specified amount
Cancel with License Revocationfalse or omittedtrueCancels + queues license deactivation job
Full Cancel PackagetruetrueCancels + refunds + queues license deactivation

Possible Responses

200Order cancelled successfully

Order cancelled (with optional refund/deactivation)

{
  "success": true,
  "data": {
    "order": {
      "id": 114,
      "status": 4,
      "canceled_at": "2025-12-29T13:00:00.000Z",
      "notes": "Customer requested cancellation"
    },
    "actions_taken": {
      "cancelled": true,
      "refund_created": false,
      "refund_amount": 0,
      "license_job_id": null
    }
  }
}
200Order cancelled with refund and license deactivation

Full cancellation package

{
  "success": true,
  "data": {
    "order": {
      "id": 114,
      "status": 4,
      "notes": "Partial refund with license revocation"
    },
    "refund": {
      "id": 88,
      "amount": 25.00,
      "status": 3,
      "provider_txn_id": "REFUND-114-1735471215789"
    },
    "actions_taken": {
      "cancelled": true,
      "refund_created": true,
      "refund_amount": 25.00,
      "license_job_id": 456
    }
  }
}
400Bad Request

Invalid order_id, refund amount exceeds available funds, or refund amount exceeds order total

{
  "error": {
    "code": 400,
    "message": "Refund amount exceeds order total"
  }
}

Notes:

  • Refund amount validation: must not exceed order.final_amount and available captured funds
  • License deactivation is asynchronous (job-based) and non-blocking
  • Multiple cancellations are idempotent when no actions are requested
  • canceled_at timestamp is set on first cancellation only

Record Payment

Record Payment

POST/order/payment

Records a payment transaction for an order identified by order code. Supports captured payments (status = 1), failed payments (status = 2), and refunds (status = 3). Automatically updates order status when fully paid and optionally enqueues license creation jobs for captured payments.

Can be called as an authenticated API request or as a webhook (with relaxed authentication).

ParameterTypeInRequiredDescription
payment.order_codestringbodyyesOrder code to apply payment to.
payment.providerstringbodyyesPayment provider name (e.g., "stripe", "paypal", "manual").
payment.provider_txn_idstringbodyyesUnique transaction ID from payment provider.
payment.amountnumber/stringbodyyesPayment amount (positive number). Accepts numeric strings.
payment.statusintegerbodyyesPayment status: 1 = captured, 2 = failed, 3 = refund.
payment.payment_methodstringbodynoPayment method type (e.g., "card", "bank_transfer").
payment.external_referencestringbodynoAdditional reference ID (e.g., customer ID, invoice number).
Example Request (Captured Payment)
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/payment" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "payment": {
      "order_code": "ORD2512290001ABC",
      "provider": "stripe",
      "provider_txn_id": "pi_3AB12CD34EF56GH78",
      "amount": 53.98,
      "status": 1,
      "payment_method": "card",
      "external_reference": "cus_ABC123XYZ"
    }
  }'
Example Request (Refund)
curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/payment" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "payment": {
      "order_code": "ORD2512290001ABC",
      "provider": "stripe",
      "provider_txn_id": "re_3ZY98WX76VU54TS32",
      "amount": 25.00,
      "status": 3,
      "external_reference": "refund-20251229"
    }
  }'

Payment Status Reference

StatusLabelDescriptionEffect on Order
1CapturedPayment successfully receivedAdds to captured total; updates order status when fully paid
2FailedPayment attempt failedRecorded for audit; no effect on order status
3RefundMoney returned to customerRecorded; does NOT reduce captured total per spec

Order Status Updates

ScenarioOrder StatusDescription
First captured payment < final_amount2 (Partially Paid)Some payment received
Captured total >= final_amount3 (Paid)Fully paid; paid_at timestamp set
Failed paymentNo changeStatus remains as-is
Refund paymentNo changeUse /order/cancel with refund for status updates

Possible Responses

200Payment recorded successfully

Payment captured and order updated

{
  "success": true,
  "data": {
    "payment": {
      "id": 234,
      "order_id": 114,
      "provider": "stripe",
      "provider_txn_id": "pi_3AB12CD34EF56GH78",
      "amount": 53.98,
      "status": 1,
      "payment_method": "card",
      "received_at": "2025-12-29T12:30:45.123Z"
    },
    "order": {
      "id": 114,
      "status": 3,
      "captured_total": 53.98,
      "paid_at": "2025-12-29T12:30:45.123Z",
      "final_amount": 53.98
    },
    "license_job": {
      "id": 789,
      "type": "create_licenses",
      "status": "pending"
    }
  }
}
200Partial payment recorded

Partial payment recorded, order not yet fully paid

{
  "success": true,
  "data": {
    "payment": {
      "id": 235,
      "amount": 25.00,
      "status": 1,
      "received_at": "2025-12-29T13:00:00.000Z"
    },
    "order": {
      "id": 114,
      "status": 2,
      "captured_total": 25.00,
      "final_amount": 53.98
    }
  }
}
400Bad Request

Missing required fields, invalid status, invalid amount, or duplicate provider_txn_id

{
  "error": {
    "code": 400,
    "message": "Invalid payment status"
  }
}
404Not Found

Order code not found in organization

{
  "error": {
    "code": 404,
    "message": "Order not found"
  }
}
409Conflict

Duplicate provider_txn_id already exists

{
  "error": {
    "code": 409,
    "message": "Payment with this transaction ID already exists"
  }
}

License Creation Jobs

When a captured payment (status = 1) brings the order to fully paid status, a background job is automatically enqueued:

{
  "queue": "default",
  "type": "create_licenses",
  "payload": {
    "order_id": 114,
    "customer_email": "john.doe@example.com"
  },
  "dedupe_key": "order:114:create_licenses",
  "priority": 100,
  "max_attempts": 5
}

Notes:

  • provider_txn_id must be unique across all payments (prevents duplicate processing)
  • Amount is stored as decimal with 2 decimal places
  • Webhook mode: relaxed authentication for payment gateway callbacks
  • Failed payments (status = 2) are recorded but don't affect order totals
  • Refund status (3) records the transaction but use /order/cancel endpoint for full refund workflow

Handle Activations from Your Website

Enable your users to manage their license activations directly from your website. This self-service flow allows users to view and optionally delete their device activations without accessing the CheckMyLicense portal.

Prerequisites: Turnstile Widget & Organization ID

Before implementing this feature you need two values from the CheckMyLicense dashboard:

  • widget_key – the Public Key of a Turnstile widget you have configured under Organization → Turnstile Widgets.
  • organization_id – your Organisation ID, shown in the Turnstile Widgets section of the Organisation page.

See the User Guide → Licences → Turnstile Widgets section for step-by-step setup instructions.

Organization ID Scope

Always include your organization_id in requests to ensure API calls are scoped to your own licenses only. This prevents unauthorized access to other organizations' license data.

Step 1: Request Access

POST/license/request-access

Initiate the activation flow by collecting the user's email, license key, Cloudflare Turnstile token, widget public key, and your organization ID. This endpoint validates the license and sends a 2FA code to the user's email.

Request Parameters

ParameterTypeInRequiredDescription
emailstringbodyyesUser email address associated with the license
licensestringbodyyesLicense key (32-character hex string)
turnstileTokenstringbodyyesCloudflare Turnstile verification token generated by the widget on your page
widget_keystringbodyyesPublic key of the Turnstile widget configured under Organization → Turnstile Widgets
organization_idstring (UUID)bodyyesYour organization ID, visible in the Turnstile Widgets section of the Organization page
Example Request
POST https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/request-access HTTP/1.1
Content-Type: application/json

{
  "email": "john.doe@example.com",
  "license": "ee5e4a58dede4a2a9fcd66f9c282c99e",
  "turnstileToken": "XXXXX",
  "widget_key": "0x4AAAAAAA_your_public_key",
  "organization_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Success Response

200 OK
{
  "success": {
    "ok": true
  }
}

Any response other than {"success":{"ok":true}} should be treated as an error. Common errors include invalid license, email mismatch, or failed Turnstile verification.

Step 2: Verify 2FA Code

POST/license/session

After the user receives the 2FA code via email, verify it to establish a session. The response includes a session token that must be used as a Bearer token for subsequent activation management calls.

Request Parameters

ParameterTypeInRequiredDescription
tokenstringbodyyes2FA verification code sent to user email
Example Request
POST https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/session HTTP/1.1
Content-Type: application/json

{
  "token": "MxP5g6WdPg-iUICxFAJP3YJ6zM-5HFc-OrB93pvymSw"
}

Success Response

200 OK
{
  "success": {
    "session_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsaWNlbnNlX2lkIjo0ODksInB1cnBvc2UiOiJsaWNlbnNlX21hbmFnZW1lbnQiLCJpYXQiOjE3NzEwNjUxOTAsImV4cCI6MTc3MTA2NjA5MH0.zUkY8iSY8kUZMPV7ZeLHVTdlblHbv--07oBYqqo8Bag",
    "expires_at": "2026-02-14T10:48:10.587Z",
    "license_id": 489
  }
}

Store the session_token securely and use it in the Authorization header for all subsequent requests. Any response other than the structure above indicates an error (invalid code, expired token, etc.).

Step 3: List Activations

POST/license/activations

Retrieve all device activations for the authenticated license. Use the session token from Step 2 as a Bearer token in the Authorization header.

Request Parameters

ParameterTypeInRequiredDescription
limitintegerbodyyesMaximum number of activations to return
offsetintegerbodyyesNumber of activations to skip (for pagination)
Example Request
POST https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/activations HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "limit": 10,
  "offset": 0
}

Success Response

200 OK
{
  "success": {
    "activations": [
      {
        "id": 291,
        "license_id": 489,
        "target_id": "565443711",
        "target_host": "Home PC",
        "type": "Live",
        "created_at": "2026-02-10T11:12:25.553795+00:00"
      },
      {
        "id": 290,
        "license_id": 489,
        "target_id": "558888747",
        "target_host": "VPS",
        "type": "Live",
        "created_at": "2026-02-08T07:35:18.04829+00:00"
      }
    ],
    "aggregate_count": 2,
    "activations_deletable": true,
    "email": "john.doe@example.com",
    "license": "ee5e4a58dede4a2a9fcd66f9c282c99e"
  }
}

Response Fields

FieldTypeDescription
activationsarrayList of activation records
aggregate_countintegerTotal number of activations (for pagination)
activations_deletablebooleanWhether user can delete activations (if true, show delete buttons)
emailstringLicense owner email
licensestringLicense key

If activations_deletable is true, display a delete button for each activation row in your UI. This allows users to remove devices they no longer use.

Step 4: Delete Activation (Optional)

POST/v1/license/activation-delete

Allow users to delete specific device activations. This is only available when activations_deletableis true in the list response. Use the session token as a Bearer token.

Request Parameters

ParameterTypeInRequiredDescription
activation_idintegerbodyyesID of the activation to delete
Example Request
POST https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/activation-delete HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "activation_id": 291
}

Success Response

200 OK
{
  "success": {
    "deleted": true
  }
}

Important Notes:

  • Only display delete buttons when activations_deletable is true
  • After successful deletion, refresh the activations list to show updated data
  • Users cannot delete activations if the license program has restrictions

Implementation Tips

User Flow

  1. Present a form with fields for Email, License Key, and Cloudflare Turnstile widget
  2. On submit, call /license/request-access with all three values plus your organization ID
  3. If successful, show a 2FA input page where users enter the code from their email
  4. Call /license/session with the 2FA code to get the session token
  5. Use the session token to call /license/activations and display the list
  6. If activations_deletable is true, add delete buttons to each row
  7. When delete is clicked, call /license/activation-delete and refresh the list

Security Best Practices

  • Always validate the Cloudflare Turnstile token on every request-access call
  • Store session tokens securely (use httpOnly cookies or secure storage)
  • Session tokens typically expire after 15 minutes - handle expiration gracefully
  • Never expose your organization ID in client-side JavaScript - make API calls from your backend
  • Implement rate limiting on your activation management pages

Error Handling

  • Any response not matching the success structure should be treated as an error
  • Display user-friendly error messages for common cases (invalid license, wrong email, expired code)
  • Implement retry logic with exponential backoff for network failures
  • Log errors for debugging but don't expose sensitive details to users
SDK Documentation

SDK Overview

CheckMyLicense provides Software Development Kits (SDKs) to simplify the integration of license verification into your applications. These SDKs handle all the complexity of encryption, API communication, and response parsing, allowing you to implement robust license protection with just a few lines of code.

Key Features

  • Easy Integration – Add license verification with minimal code changes
  • Secure Encryption – HMAC-SHA256 encryption for data signatures
  • Offline Support – Cached offline tokens for verification without network access
  • Program Configuration – Dynamic feature management via server-side configuration
  • Detailed Error Handling – Clear error messages for troubleshooting

Available SDKs

To download the available SDKs, please visit your organization page: Available SDK section.

  • MQL5 SDK (MetaTrader) – ✅ Production-Ready - Add license verification to MetaTrader 4 or 5 EA and indicators.
  • Python SDK v3 – ✅ Production-Ready - Command-line tool and library with offline tokens, program configuration, and HMAC-SHA256 authentication.

More SDKs Coming Soon: We're actively developing SDKs for additional platforms including JavaScript/Node.js, C#/.NET, and Java. If you need an SDK for a specific platform, please contact our support team.

MQL5

The MQL5 SDK enables seamless license verification for MetaTrader 5 Expert Advisors, Indicators, and Scripts. Protect your trading tools with enterprise-grade license management.

Installation

Important: Always enable Cloud Protect and rebuild regularly

When compiling your Indicator or Expert Advisor (EA), always enable Cloud Protect. Using cloud protection adds an extra layer of security for your published EA and helps prevent unauthorized redistribution. Never share an EA without cloud protection enabled.

  • Enable Cloud Protect every time you compile your indicator or EA.
  • Never distribute an unprotected EA — always use cloud protection for published builds.
  • Periodically rebuild your programs using newer MetaTrader builds and SDK versions to benefit from security fixes and compiler improvements.

Prerequisites

  • MetaTrader platform installed
  • MetaEditor installed
  • Active CheckMyLicense organization account
  • Your organization's API Key and Secret (found in Settings → Organization)

Installation Steps

  1. Download the SDK
  2. Extract Files

    The SDK package contains two files:

    • CheckMyLicense.mqh – The reusable SDK library
    • SHA26.mqh – The reusable component for SHA-256 hashing
    • CheckMyLicense.mq5 – Example implementation script
  3. Copy to MetaTrader Directory

    Copy CheckMyLicense.mqh and SHA26.mqh to your MetaTrader 5 Include folder:

    C:\Users\YourUsername\AppData\Roaming\MetaQuotes\Terminal\[TerminalID]\MQL5\Include\
  4. Allow WebRequest URL

    In MetaTrader 5, go to Tools → Options → Expert Advisors and add the CheckMyLicense API URL to the list of allowed URLs:

    https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1

Important: Without adding the URL to allowed WebRequest addresses, the SDK will not be able to communicate with CheckMyLicense servers, and license verification will fail.

Quick Start

Basic Implementation

Add license verification to your Expert Advisor, Indicator, or Script:

1. Define Input Parameters

input string InpEmail = "";    // User&apos;s email
input string InpLicence = ""; // License key

2. Verify License OnInit for Expert Advisors

int OnInit()
{
    if(!CheckMyLicense(InpEmail, InpLicence))
    {
        Print("License verification failed!");
        return INIT_FAILED;
    }
    
    Print("License verified successfully!");
    return INIT_SUCCEEDED;
}

3. Verify License OnStart for Scripts

void OnStart()
{
    if(!CheckMyLicense(InpEmail, InpLicence))
    {
        Print("License verification failed!");
        return;
    }
    
    // Your script logic here
    Print("Running licensed script...");
}

Examples

Complete Expert Advisor Example

Here's a complete example of an Expert Advisor with license verification:

//+------------------------------------------------------------------+
//|                                            LicensedEA.mq5 |
//|                                  Copyright 2025, Your Company |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Your Company"
#property link      "https://www.yourwebsite.com"
#property version   "1.00"

string name = "Your Program Name"
#include "CheckMyLicense.mqh"

// Input parameters
input string InpEmail = "";              // Email
input string InpLicence = "";            // License Key
input double InpLotSize = 0.01;          // Lot Size
input int    InpMagicNumber = 12345;     // Magic Number

//+------------------------------------------------------------------+
//| Expert initialization function                                     |
//+------------------------------------------------------------------+
int OnInit()
{
    // Verify license first
    if(!CheckMyLicense(InpEmail, InpLicence))
    {
        Alert("License verification failed! EA will not run.");
        return INIT_FAILED;
    }
    
    Print("License verified! EA starting...");
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert tick function                                              |
//+------------------------------------------------------------------+
void OnTick()
{
    // Your trading logic here
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    Print("EA stopped. Reason: ", reason);
}

Indicator Example

Important Limitation: MetaTrader does not allow Indicators to call WebRequest() functions for security reasons. If you need to license a custom indicator, you must create a wrapper Expert Advisor that verifies the license and then calls the indicator logic. Alternatively, you can distribute your indicator as part of a licensed EA template.

If you must use an Indicator with license verification, create an EA wrapper that loads the indicator as an embedded resource. This approach requires only license verification on the EA, not the indicator itself:

//+------------------------------------------------------------------+
//|                         CustomIndicatorWrapper.mq5 |
//|  EA that attaches a custom licensed indicator to the chart      |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Your Company"
#property link      "https://www.yourwebsite.com"
#property version   "1.0"

string name = "Licensed Indicator Wrapper"
#include "CheckMyLicense.mqh"

// Embed your custom indicator as a resource
#resource "CustomIndicator.ex5"

// Resource reference for iCustom
#define CUSTOM_INDICATOR "::CustomIndicator.ex5"

// Mirror indicator inputs to match your custom indicator parameters
input int    InpPeriod            = 14;       // MA Period
input int    InpShift             = 0;        // Shift
input bool   InpShowSignal        = true;     // Show Signal Line

// License input
input string InpEmail = "";
input string InpLicence = "";

// Internal
int g_ind_handle = INVALID_HANDLE;
int ext_reason = 0;

//+------------------------------------------------------------------+
//| Helper: Remove indicator from chart if present                   |
//+------------------------------------------------------------------+
void RemoveIndicator()
{
    if(g_ind_handle != INVALID_HANDLE)
    {
        // Find and remove any indicator with matching name
        int total = (int)ChartIndicatorsTotal(0, 0);
        for(int i = total - 1; i >= 0; --i)
        {
            string name = ChartIndicatorName(0, 0, i);
            if(StringFind(name, "CustomIndicator") >= 0)
                ChartIndicatorDelete(0, 0, name);
        }
        IndicatorRelease(g_ind_handle);
        g_ind_handle = INVALID_HANDLE;
    }
}

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // Verify license first
    if(ext_reason != 3) // Skip on EA reload (reason 3)
    {
        if(!CheckMyLicense(InpEmail, InpLicence))
        {
            Alert("License verification failed! EA will not run.");
            return INIT_PARAMETERS_INCORRECT;
        }
    }

    // Ensure any previous instance is removed
    RemoveIndicator();

    // Create indicator handle with your custom parameters
    g_ind_handle = iCustom(
        _Symbol,
        _Period,
        CUSTOM_INDICATOR,
        InpPeriod,
        InpShift,
        InpShowSignal
    );

    if(g_ind_handle == INVALID_HANDLE)
    {
        PrintFormat("Failed to create indicator handle. Error=%d", GetLastError());
        return INIT_FAILED;
    }

    // Add indicator to chart
    if(!ChartIndicatorAdd(0, 0, g_ind_handle))
    {
        PrintFormat("ChartIndicatorAdd failed. Error=%d", GetLastError());
        IndicatorRelease(g_ind_handle);
        g_ind_handle = INVALID_HANDLE;
        return INIT_FAILED;
    }

    Print("License verified! Custom indicator attached to chart.");
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    RemoveIndicator();
    if(reason == 3)
    {
        ext_reason = 3; // Track EA reload
    }
}

//+------------------------------------------------------------------+
//| Expert tick function (no trading logic needed)                   |
//+------------------------------------------------------------------+
void OnTick()
{
    // Wrapper runs silently; indicator handles all logic
}

Understanding Error Codes

The SDK uses specific error codes to help diagnose issues during license verification:

Error CodeDescriptionSolution
EX1001Email not providedEnter your email address in the input parameters (InpEmail)
EX1002License key not providedEnter your license key in the input parameters (InpLicence)
EX1003Program name not configuredSet #define ProgramName to match your program name in CheckMyLicense
EX1004License validation failed at startupVerify your license key format and API credentials
EX2001Payload encoding failedVerify your #define APISecret is correctly set from Organization settings
EX2002HMAC-SHA256 signature computation failedVerify #define APIID and #define APISecret are correct
EX3001WebRequest URL not allowed in MT5Add https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1 to Tools → Options → Expert Advisors → Allow WebRequest for URLs
EX3003Connection error or timeoutCheck your internet connection; if persists, verify the server URL is accessible
EX4001-EX4005Cryptographic operation failed (Base64 encoding/decoding)Verify the license key is valid; check that SHA256.mqh is included in your project
400 Bad RequestInvalid payload or malformed requestVerify email format is correct and license key contains valid 32 hex characters
401 UnauthorizedLicense invalid, expired, or not owned by emailVerify the email matches the license owner; check license hasn't expired in your dashboard
404 Not FoundLicense or program not foundVerify the license key exists and the program name matches your CheckMyLicense dashboard
500 Server ErrorCheckMyLicense server errorTry again later; if error persists, contact support

Testing Your Implementation

  1. Compile – Compile your MQL5 file in MetaEditor (F7)
  2. Attach to Chart – Drag your EA/Indicator to a chart
  3. Enter Credentials – Input your email and license key in the parameters dialog
  4. Check Logs – Open the Experts tab to see verification results
  5. Verify Activation – Check your CheckMyLicense dashboard for the new activation

Best Practice: Always verify the license during initialization (OnInit). This prevents unauthorized use and ensures proper activation tracking from the start. Also consider implementing periodic re-validation during runtime for long-running applications.

Python SDK v3

The Python SDK v3 provides a command-line tool and library for integrating CheckMyLicense verification into Python applications. It handles HMAC-SHA256 authentication, request signing, offline token caching, and program configuration management automatically.

Key Features

  • Protocol v3 Support – Default v3 with program configuration; backward compatible with v1/v2
  • Program Configuration Access – Global PROGRAM_CONFIGURATION variable for runtime feature management
  • Offline Token Caching – Automatic token caching to ~/.cml_offline/ for offline verification
  • Command-Line Interface – Verify licenses directly from terminal or scripts
  • Server Signature Verification – Built-in validation of server responses
  • Flexible Integration – Use as CLI tool or import as Python module

What's New in v3

  • Program Configuration – Server returns program-specific settings in response
  • Offline Tokens – Cached tokens include configuration for offline use (7-day validity)
  • Enhanced Security – v3 offline tokens include configuration in signature to prevent tampering
  • Version Selection – Choose protocol version with --v flag (default: 3)

Installation

Prerequisites

  • Python 3.7 or higher
  • requests library for HTTP calls
  • cryptography library for token validation
  • Active CheckMyLicense organization account
  • Your organization's API ID and API Secret (from Settings → Organization)

Installation Steps

  1. Install Dependencies
    pip install requests cryptography
  2. Download the SDK

    Download the Python SDK v3 from your organization page (Available SDK section).

  3. Extract the File

    The SDK package contains a single file:

    • CheckMyLicenseOnline.py – The CLI tool and library
  4. Verify Installation
    python CheckMyLicenseOnline.py --help

Offline Token Storage: Offline tokens are automatically cached to ~/.cml_offline/{license}.json after successful online verification. This directory is created automatically if it doesn't exist.

Quick Start

Online Verification with Configuration (v3)

Verify a license and retrieve program configuration:

python CheckMyLicenseOnline.py \
  --api-id a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d \
  --api-secret YOUR_API_SECRET \
  --endpoint "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1" \
  --program "My Python Application" \
  --license aBcD1234eFgH5678-iJkL9012-mNoP3456-qRsT7890-uVwXyZ12 \
  --email user@example.com \
  --v 3 \
  --show-config

Example Output

License verified successfully!
Validity: 2026-12-31 23:59:59
Activations: 2/5
Offline token saved to ~/.cml_offline/aBcD1234eFgH5678-iJkL9012-mNoP3456-qRsT7890-uVwXyZ12.json

Program Configuration:
{
  "max_devices": 5,
  "advanced_features": true,
  "api_rate_limit": 1000,
  "custom_branding": "Premium"
}

Offline Verification

Use cached offline token for verification without network access:

python CheckMyLicenseOnline.py \
  --program "My Python Application" \
  --license aBcD1234eFgH5678-iJkL9012-mNoP3456-qRsT7890-uVwXyZ12 \
  --offline \
  --show-config

Offline Output

Using offline token (expires: 2026-02-12 15:30:00)
License verified successfully (offline mode)

Program Configuration:
{
  "max_devices": 5,
  "advanced_features": true
}

Command-Line Parameters

ParameterRequiredDescription
--api-idOnline onlyOrganization API ID from Settings → Organization
--api-secretOnline onlyOrganization API Secret from Settings → Organization
--endpointOnline onlyCheckMyLicense API endpoint URL
--licenseYesLicense key
--emailOnline onlyLicense owner's email address
--programYesProgram name (must match CheckMyLicense)
--vNoProtocol version: 1, 2, or 3 (default: 3)
--offlineNoUse cached offline token if available
--show-configNoDisplay program_configuration from response
--target-idNoTarget machine/device identifier (optional)

Protocol Versions

VersionFeatures
v3 (default)Includes program_configuration, offline_token with config in signature
v2Basic response with validity, limit, used (no configuration)
v1Legacy basic response

Offline Token Behavior: Offline tokens are valid for 7 days from issuance. v3 offline tokens include the program configuration in both the payload and cryptographic signature, preventing configuration tampering. After the first successful online verification, subsequent calls can use--offline flag for instant verification without network access.

Examples

Programmatic Integration - Basic

Import and use the SDK in your Python application with v3 configuration access:

import sys
from CheckMyLicenseOnline import verify_license, PROGRAM_CONFIGURATION

def main():
    # Verify license
    result = verify_license(
        api_id="your-api-id",
        api_secret="your-secret",
        endpoint="https://your-endpoint",
        program="My Python Application",
        license_key="user-license-key",
        email="user@example.com",
        version=3
    )
    
    if not result['success']:
        print(f"License verification failed: {result['message']}")
        sys.exit(1)
    
    print("License verified successfully!")
    print(f"Valid until: {result['validity']}")
    print(f"Activations: {result['used']}/{result['limit']}")
    
    # Access program configuration
    max_devices = PROGRAM_CONFIGURATION.get('max_devices', 1)
    features_enabled = PROGRAM_CONFIGURATION.get('advanced_features', False)
    
    print(f"Max devices allowed: {max_devices}")
    print(f"Advanced features: {'enabled' if features_enabled else 'disabled'}")
    
    # Your application logic here
    run_application(features_enabled)

if __name__ == "__main__":
    main()

Offline-First Approach

Try offline verification first, then fall back to online if needed:

from CheckMyLicenseOnline import verify_license, verify_offline, PROGRAM_CONFIGURATION

def verify_with_fallback(license_key, **online_params):
    """Try offline verification first, fall back to online"""
    
    # Try offline verification
    offline_result = verify_offline(
        program=online_params['program'],
        license_key=license_key
    )
    
    if offline_result['success']:
        print("Verified using cached offline token")
        return offline_result
    
    # Fall back to online verification
    print("Offline token unavailable, verifying online...")
    online_result = verify_license(
        **online_params,
        license_key=license_key
    )
    
    return online_result

# Usage
result = verify_with_fallback(
    license_key="user-license",
    api_id="your-api-id",
    api_secret="your-secret",
    endpoint="https://your-endpoint",
    program="My App",
    email="user@example.com",
    version=3
)

Configuration-Driven Features

Use program configuration to enable/disable features dynamically:

from CheckMyLicenseOnline import verify_license, PROGRAM_CONFIGURATION

def check_license_and_configure():
    result = verify_license(
        api_id="your-api-id",
        api_secret="your-secret",
        endpoint="https://your-endpoint",
        program="Professional CAD Tool",
        license_key=user_license,
        email=user_email,
        version=3
    )
    
    if not result['success']:
        raise Exception(f"License invalid: {result['message']}")
    
    # Extract configuration
    config = PROGRAM_CONFIGURATION
    
    # Apply configuration to application settings
    app_config = {
        'max_projects': config.get('max_projects', 5),
        'export_formats': config.get('export_formats', ['pdf']),
        'cloud_sync': config.get('cloud_sync_enabled', False),
        'plugin_access': config.get('plugins', []),
        'render_quality': config.get('render_quality', 'standard')
    }
    
    return app_config

# Initialize application
try:
    settings = check_license_and_configure()
    print(f"Application configured: {settings}")
    launch_application(settings)
except Exception as e:
    print(f"Startup failed: {e}")
    sys.exit(1)

Response Data Structure

v3 Online Response

{
  "status": "success",
  "message": "License verified successfully",
  "validity": 1735689600,
  "limit": 5,
  "used": 2,
  "program_configuration": {
    "max_devices": 5,
    "advanced_features": true,
    "api_rate_limit": 1000,
    "custom_setting": "value"
  },
  "offline_token": "eyJwcm9ncmF...",
  "offline_expires_at": 1739462400
}

v3 Offline Token Payload

{
  "program_name": "My Python Application",
  "license_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "expires_at": 1739462400,
  "target_ids": "device-123,device-456",
  "configuration": {
    "max_devices": 5,
    "advanced_features": true
  },
  "sig": "a1b2c3d4e5f6..."
}

Desktop Application Pattern

Complete license manager class for desktop applications:

class LicenseManager:
    def __init__(self, app_name, api_credentials):
        self.app_name = app_name
        self.api_creds = api_credentials
        self.config = {}
        
    def activate(self, license_key, email):
        """Activate application with license"""
        from CheckMyLicenseOnline import verify_license, PROGRAM_CONFIGURATION
        
        result = verify_license(
            api_id=self.api_creds['id'],
            api_secret=self.api_creds['secret'],
            endpoint=self.api_creds['endpoint'],
            program=self.app_name,
            license_key=license_key,
            email=email,
            version=3
        )
        
        if result['success']:
            self.config = PROGRAM_CONFIGURATION.copy()
            return True, "Activation successful"
        
        return False, result.get('message', 'Activation failed')
    
    def get_feature_access(self, feature_name):
        """Check if feature is enabled via configuration"""
        features = self.config.get('enabled_features', [])
        return feature_name in features
    
    def get_resource_limit(self, resource_name):
        """Get resource limit from configuration"""
        limits = self.config.get('resource_limits', {})
        return limits.get(resource_name, 0)

# Usage
license_mgr = LicenseManager(
    "Professional Video Editor",
    {'id': 'api-id', 'secret': 'api-secret', 'endpoint': 'https://...'}
)

success, message = license_mgr.activate(user_license, user_email)
if success:
    if license_mgr.get_feature_access('4k_export'):
        enable_4k_export()
    
    max_projects = license_mgr.get_resource_limit('max_projects')
    configure_project_limit(max_projects)

Migration from v2 to v3

v3 is backward compatible. Simply update the version parameter:

# Old (v2)
result = verify_license(..., version=2)

# New (v3) - explicit
result = verify_license(..., version=3)

# New (v3) - implicit (default)
result = verify_license(...)  # Uses v3 by default

# Access configuration after v3 verification
from CheckMyLicenseOnline import PROGRAM_CONFIGURATION
config = PROGRAM_CONFIGURATION
max_users = config.get('max_users', 1)

Best Practices: Always use .get() with defaults when accessing PROGRAM_CONFIGURATION to handle programs with no configuration set. Store API credentials in environment variables, never hardcode them in source code. Implement periodic re-verification (e.g., every 24 hours) for long-running applications.

Security Note: v3 offline tokens include the configuration in the cryptographic signature, preventing tampering. If the configuration in the token payload doesn't match the signature, verification will fail. This ensures configuration integrity even in offline mode.

License Management Documentation | CheckMyLicense API, MQL4/MQL5 SDK & MetaTrader Guides