Documentation

Learn how to use CheckMyLicense with step-by-step guides, API reference, and SDK 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, whitelists, 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 manage versions, click the eye icon next to any program to open its detail page, then use Add Version or click any row in the versions table to view it.

Program Versions

Each program can have multiple versions. The versions table shows each version's number, created date, status (Released or Draft) and title. Clicking a row opens the version in view mode.

Creating or Editing a Version

Click Add Version or the edit icon on an existing version. The form contains the following fields:

  • Version Number — the release identifier (e.g. 1.3.0). The form suggests the next logical version based on existing versions for the same program.
  • Release Title — a short headline for the release (e.g. “Performance & Stability Update”). This is used as the email subject when the version is released to licence holders.
  • Release Email Body — a rich Markdown editor. Use headings, bold text, bullet lists, links, and more to compose the notification email your licence holders will receive. A live preview pane on the right of the editor renders the formatted output as you type, so you can verify the layout before saving.
  • Program File / Link — choose one of two modes via the toggle:
    • Upload File — pick a local file; it is stored in secure cloud storage and a download URL is saved automatically. If the version already has a file, uploading a new one replaces it.
    • Share Link — paste an external URL (e.g. a download page or CDN link). No file is uploaded; the URL is saved directly as the version's link.

Version Status

Every version starts with a Draft status. A version becomes Released only when the Release button is used to send the release notification email (see below). The status is read-only and cannot be changed manually — it reflects whether the release email has been dispatched.

Releasing a Version

When viewing a version that is still in Draft status, a green Release button appears in the modal footer. Clicking it:

  1. Sends a formatted release notification email to every active licence holder of the program, using the Release Title as the subject and the rendered Release Email Body as the HTML content.
  2. Marks the version as Released — the status badge updates immediately in both the modal and the versions table.
  3. Displays a summary showing how many emails were sent and, if any, how many failed with reasons.

Tip: Use the Share Link option when distributing through a marketplace or external download portal, and Upload File when you want to host the binary directly on the platform. Either way, the link is included in the release email so recipients can access the new version immediately.

Important: Releasing a version is irreversible — once sent, the status cannot be reset to Draft and the emails cannot be recalled. Make sure the Release Email Body and Release Title are correct before clicking Release.

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.

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. You are automatically taken to the licence detail page where you can copy the key, send it by email, and configure Access Rules — see Access Rules below for details.

Access Rules

Access rules let you restrict which activations are accepted for a licence. Each rule combines up to three criteria — Type, Target ID, and Target Host — into a single row. An incoming activation is accepted when it matches all non-empty fields within at least one rule.

  • Type — the activation type, e.g. live or demo. Leave blank to match any type.
  • Target ID — a specific account number, machine ID, or other unique identifier. Leave blank to match any target ID.
  • Target Host — a server name or hostname, e.g. UltimaMarkets-Live 1. Leave blank to match any host.

Rules are managed exclusively from the licence detail page (Licences → select a licence). Use + Add Rule to create a rule, the toggle icon on each row to activate or deactivate it individually, and the delete icon to remove it permanently.

The Specific Rules toggle at the top of the Access Rules table acts as a master switch. When it is OFF, all rules are ignored and every activation attempt is accepted (subject to limit and expiry). Turning it ON enforces the rule set. The toggle can be flipped at any time without deleting the rules.

In the edit modal you can toggle individual rules Active / Inactive, but adding or removing rules must be done from the licence detail page.

Rule matching logic: An activation is accepted when it satisfies every non-empty field of at least one active rule. For example, a rule with Type = live and Target Host = UltimaMarkets-Live 1 will accept any activation that is both type live and from that host — the Target ID field being blank means any Target ID is permitted for that rule. A licence with multiple rules will accept an activation that passes any one of them.

License Management

After creation, the licence status and Activations Deletable flag can be edited from the edit modal. Access rules are managed from the licence detail page. Each licence 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 deploy the open-source CML Activations Web SDK on your own website. The starter includes the My Activations API flow, email verification, and Cloudflare Turnstile.

Choose the activation access scope that fits your website:

  • mode=license without an organization or program asks for email and license.
  • mode=org with an organization asks for email and shows licenses across that organization.
  • mode=org with an organization and program asks for email and presents licenses for that program.
  • mode=license with an organization asks for email and license, while constraining the license to that organization. A program can also be linked.

Try the live demo. To test the complete flow, create a temporary license through the CML Marketplace, place the order using your email, and use the issued license on the activations page.

See Handle Activations from Your Website for configuration and API details.

Linked Product

When a licence is linked to a product (i.e. its product_id is set), the licence detail page shows a Linked Product row in the info panel displaying the product name.

To remove the link, click the unlink icon (broken chain) next to the product name. A confirmation dialog is shown before the change is applied. Once confirmed, product_id is set to null and the row disappears from the Inactive Licenses tab on the Whitelist page.

Note: Unlinking a product does not delete the licence or its access rules. It only removes the association between the licence and the product, which means the licence will no longer appear in whitelist-related workflows.

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.

Ready-to-use Marketplace SDK: Launch a marketplace and start selling the products you have configured in CheckMyLicense in minutes. Access and download the source code from MitalioHQ/cmlmarketplace, and see it in action at marketplace.checkmylicense.dev.

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 Price. The product automatically uses the organisation's e-commerce currency.
  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. Enable Whitelist Rules if this product should enforce whitelist-based activation restrictions. When checked, a chip selector appears — choose which Whitelists to attach to this product. Licences generated from this product will inherit those whitelist rules.
  6. Write a description for storefront display.
  7. Click Create. The product appears with actions to view, edit or delete.

Sales Channels

Sales channels define the catalogs used by your storefronts. Each channel has a stable slug, public links, geographic availability, and an ordered selection of products. The protected Portal channel records orders created manually in this dashboard.

  1. Open Sales Channels under E-Commerce and select Create channel.
  2. Set the channel identity and public links. Keep a new channel inactive while configuring it.
  3. Choose All countries or select allowed countries from the database-backed continent groups.
  4. Map products, choose which are visible, set their display order, and optionally choose one visible Default product per program. Multiple non-default products under the same program are allowed.
  5. Save, then use Preview catalog with an allowed and restricted country before activating the channel.

Customer and organization countries use ISO 3166-1 alpha-2 codes from the database. Older free-text country values remain visible until a standard country is selected during editing.

Storefronts read the explicit default flag; they must not infer it from a product title, price, validity, or position. Program codes are required and unique within the organization. A program code becomes locked after the program is first published to an external sales channel because integrations such as Trade't store it as cmlProgramCode.

Whitelist

Whitelists are reusable collections of activation rules that can be attached to one or more products. Instead of setting access rules on every individual licence, you define the allowed activation criteria once in a whitelist and attach it to the relevant products. When an order generates a licence, the whitelist rules are applied to restrict which activations are accepted.

Create a Whitelist

  1. Navigate to Whitelist (under E‑Commerce) and click Add Whitelist.
  2. Enter a Name for the whitelist (e.g. “Live Accounts Only” or “VIP Servers”).
  3. Click Save. The whitelist appears in the table showing its name, item count and creation date.

Manage Whitelist Items

Click any whitelist row to open its detail page. Each whitelist contains a table of items — individual rules that define which activations are permitted.

  1. Click Add Item (or the inline form above the table).
  2. Fill in any combination of the three criteria:
    • Type — activation type, e.g. live or demo. Leave blank to match any type.
    • Target ID — account number or machine identifier. Leave blank to match any ID.
    • Target Host — server or hostname, e.g. UltimaMarkets-Live 1. Leave blank to match any host.
  3. Click the check button to save the item. It appears in the table immediately.
  4. Use the toggle icon to activate or deactivate individual items without deleting them.
  5. Use the delete icon to permanently remove an item.

Bulk Add or Update Whitelist Items

Open a whitelist's detail page and click Bulk add/update above the Items table. Each operation accepts up to 50 rows. Choose the method that matches your data:

Excel file

  1. Choose Excel file and click Download Excel template. Always use the downloaded template rather than changing its columns.
  2. Enter one item per row. target_id is required; type, target_host and active are optional. Set active to true or false.
  3. Keep the workbook as a single .xlsx worksheet no larger than 2 MB. The required columns are target_id, type, target_host and active. Use plain values only; formulas are not accepted.
  4. Upload the completed workbook. Existing items are matched by Target ID. Nonblank optional cells update the matching item; blank optional cells keep its current values. This version does not clear an existing field through Excel.

Quick list

  1. Choose Quick list, then select whether your pasted values represent Target ID, Type or Target host / Server ID.
  2. Paste up to 50 comma-separated or newline-separated values.
  3. Quick List creates missing values only. A value already present in the whitelist remains unchanged.
  4. Be careful when importing only a Type or Target Host: the other rule fields stay blank, creating a wildcard that can match any value in those fields.

Review and run

  1. Click Review preview and verify the displayed rows and any wildcard warning.
  2. Check the confirmation box, then click Confirm and run.
  3. Review the summary of inserted, updated, unchanged and rejected rows. Click Download results for the complete row-by-row report.
  4. If you safely retry the same input, existing values are updated or reported as unchanged rather than inserted again.

After importing: Use Refresh Licenses from the Whitelist Lists page when existing live licences must receive the updated whitelist rules.

Matching logic: An activation passes if it satisfies every non-empty field of at least one active item across all whitelists attached to the product. The logic is identical to licence-level Access Rules.

Attach a Whitelist to a Product

Open the Product edit modal, enable Whitelist Rules, then click the whitelist name chips that should apply. Multiple whitelists can be attached. The rules from all attached whitelists are combined — an activation is accepted if it matches any active item in any of the attached whitelists.

Refresh Licenses

After updating a whitelist's items, click the Refresh Licenses button (the circular arrow icon) on any row in the Whitelist Lists table. This enqueues a background job that propagates the latest whitelist rules to all licences linked to the products that use this whitelist.

  • The job runs asynchronously — a confirmation notification shows how many licences were enqueued and how many were skipped because a job was already pending or running.
  • If you click the button a second time while the previous job is still running, the server deduplicates and skips those licences. Once the worker finishes, a fresh job can be triggered again.

Tip: Always click Refresh Licenses after editing whitelist items so that live licences immediately reflect the latest rules. Without a refresh, existing licences continue using the cached rule set from their last update.

Inactive Licenses Tab

The Whitelist page has two tabs: Whitelist Lists (the main list of whitelists) and Inactive Licenses. The Inactive Licenses tab shows all licences that are currently inactive and are linked to a product, expanded one row per access rule so each Target ID / Target Host pair is visible individually.

Columns shown: Product, Customer, Created, Target ID, Target Host, and an eye icon to navigate directly to the licence detail page.

Adding Inactive License Rules to a Whitelist

Use this workflow to bulk-populate a whitelist directly from your customers' existing licence access rules:

  1. Open the Inactive Licenses tab.
  2. Check the rows whose Target ID / Target Host you want to add. Use the header checkbox to select all visible rows at once.
  3. Click Add to List (N) — a modal appears listing all your whitelists.
  4. Select the target whitelist from the dropdown and click Add to Whitelist.
  5. The portal checks for duplicates and inserts only new entries. A summary notification confirms the result:
    • X added — new items inserted successfully.
    • Y already exist — skipped because the same Target ID is already in the whitelist.
    • Z errored — individual items that failed to insert.
  6. After a successful add, the Refresh Licenses job is triggered automatically for the chosen whitelist.

Note: Rows without a Target ID (e.g. access rules that only define a type) are not selectable and will be reported as skipped (no target) if somehow included in the selection.

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 (all required).
  3. Optionally select the Type (Individual or Business), enter the Postal Code, Country, Phone, State, Address and Tax ID.
  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.

Each referral code also has a Type which controls the commission model:

  • Affiliate — the referee refers customers and earns a commission from you on paid orders. Set a Commission Rate % (what the referee earns) and optionally a Cost Rate % (your operating cost, deducted before commission is calculated). Formula: commission = (order amount − cost) × commission rate.
  • Reseller — the partner sells on your behalf and owes you the full order amount. Commission rate is not applicable; set it to 0.
  • Sub-Affiliate — a two-party arrangement where the referee (the parent) has recruited a sub-affiliate partner (the child). The referee earns the full commission from you as usual, but a portion of it (Parent Share %) is reserved for the parent referee to pass on to the child. Set a Commission Rate %, Cost Rate % and Parent Share %. The parent share is an absolute percentage of the net amount — not a percentage of the commission.

Commission Formula

For Affiliate codes, commission is calculated on Paid orders only:

cost        = original_amount × (cost_rate ÷ 100)
net         = final_amount − cost
commission  = net × (commission_rate ÷ 100)

Set Cost Rate to 0 if you do not want to deduct operating costs before calculating commission. Cancelled and refunded orders always show $0 commission.

Example A — Affiliate with cost deduction

Contract: 20% discount to customer, 10% commission to referee, 20% cost rate. Product listed at $100.

Customer pays  = $100 − 20%          = $80.00  (final_amount)
Cost           = $100 × 20%          = $20.00
Net            = $80.00 − $20.00     = $60.00
Commission     = $60.00 × 10%        = $6.00   ← you owe the referee

Example B — Affiliate, no cost deduction

Contract: 15% discount to customer, 15% commission to referee, 0% cost rate. Product listed at $200.

Customer pays  = $200 − 15%          = $170.00 (final_amount)
Cost           = $200 × 0%           = $0.00
Net            = $170.00 − $0.00     = $170.00
Commission     = $170.00 × 15%       = $25.50  ← you owe the referee

Example C — Reseller code

Contract: 30% discount, commission rate 0% (reseller type). Product listed at $100.

Customer pays  = $100 − 30%          = $70.00  (final_amount)
Commission     = $0.00               (reseller type — partner owes YOU $70.00)

Sub-Affiliate Commissions

When the referral type is Sub-Affiliate, the commission calculation has an extra split step. You pay the referee (parent) their full commission; the report then shows how much of that commission the parent is expected to pass on to their sub-affiliate partner (child). CheckMyLicense does not process the child payout — you pay the parent, and they settle with the child.

cost           = original_amount × (cost_rate ÷ 100)
net            = final_amount − cost
commission     = net × (commission_rate ÷ 100)   ← total you owe the parent
parent_share   = net × (parent_share_rate ÷ 100) ← parent keeps this
sub_amount     = commission − parent_share        ← parent passes this to the child

Parent Share % is an absolute percentage of the net amount — not a percentage of the commission.

Example D — Sub-Affiliate

Contract: 10% customer discount, 30% commission rate, 20% cost rate, 5% parent share. Product listed at $1,000.

Customer pays  = $1,000 − 10%        = $900.00  (final_amount)
Cost           = $1,000 × 20%        = $200.00
Net            = $900.00 − $200.00   = $700.00
Commission     = $700.00 × 30%       = $210.00  ← you pay this to the parent
Parent share   = $700.00 × 5%        =  $35.00  ← parent keeps this
Sub amount     = $210.00 − $35.00   = $175.00  ← parent passes to the child

Tiered Commission

Both Affiliate and Sub-Affiliate codes support tiered commission. Instead of a single flat rate, you define multiple tiers — each starting at a certain number of paid orders in the current reset window. As the referee accumulates paid orders within the window, the applicable rate advances automatically.

  • Switch Commission Model to Tiered when creating or editing a referral code.
  • Click Add Tier and set Starts at paid order # and Commission % for each tier. Sub-Affiliate codes also have a Parent Share % per tier.
  • Set a Tier Reset period: Weekly (resets every Monday), Monthly, Quarterly, Yearly or Never. With Never, the order count accumulates indefinitely.
  • The report always applies the correct tier rate for the window each order fell in — historical orders are not affected when you change the reset period going forward.

Commission figures in the Referee Performance Report are informational only. CheckMyLicense does not process payouts automatically — use the report to calculate what you owe affiliates and to reconcile reseller invoices manually.

Referee Performance Report

The Referee Performance Report gives you a consolidated view of all orders linked to referral codes. Access it by clicking the Report button on the Referees list page.

Filters

  • Referee – narrow the report to a single referee or leave blank to include all.
  • From / To – date range based on the order's paid date. Leave blank for all dates.
  • Status – filter by order status (Paid, Pending, Cancelled, etc.) or leave as All.
  • Type – filter by referral type: Affiliate, Sub-Affiliate or Reseller.

Summary Cards

The top row shows aggregated totals for the current filter selection: Total Orders, Total Turnover, Discount Given, Paid, Pending, Cancelled, Refunded, Commission Owed (total commission due to affiliates on paid orders), and Reseller Revenue (total amount resellers owe you on paid orders).

Table Columns

  • Referee – the affiliate who owns the referral code. Click to open the referee's detail page.
  • Referral Code – the code applied to the order. Click to open the referral detail page.
  • Type – Affiliate, Sub-Affiliate or Reseller badge.
  • Code Discount – the discount configured on the referral code (percentage or fixed amount).
  • Order – the order reference code. Click to open the order detail page.
  • Status – current order status.
  • Amount – final order amount after discount.
  • Order Discount – the actual discount value applied to that order.
  • Commission – commission owed to the referee for paid orders (affiliate / sub-affiliate type only). Calculated as: (final amount − cost) × commission rate.
  • Parent % / Parent Amt – for Sub-Affiliate codes only: the share of the net amount the parent referee keeps before paying the child. Parent Amt = net × parent share %.
  • Sub Amt – for Sub-Affiliate codes only: the amount the parent is expected to pass to the child. Sub Amt = commission − parent amt.
  • Paid At – date the order was paid.
  • Cancelled Date – date the order was cancelled (if applicable).

Exports

  • CSV – downloads all matching rows (up to 2,000) as a spreadsheet-ready file.
  • Print / PDF – opens a print-ready page with CheckMyLicense branding. Use your browser's Save as PDF option to export as a PDF.

Customers

Customers are available from Operations → Customers on every package. Open a customer to add a licence directly; the customer email is filled in automatically. Packages with the E-Commerce capability also show the customer's Orders tab and Add Order action.

  1. Go to Operations → Customers and click Add Customer.
  2. Enter the Email, First Name and Last Name (all required).
  3. Optionally select the Type (Individual or Business), enter the Postal Code, Country, Phone, State, Address and Tax ID. Click Create.
  4. 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. Optionally enter a Target ID and/or Target Host per line item. When provided, these values are stored in the order item metadata and will be used to pre-populate access rules on the licences generated from that item after payment.
  4. Click Create Order. A confirmation modal shows the order code and total. Confirming sets the status to Draft. Open the order details to review.
  5. 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

On Growth and Pro, members are added through the administrator-only Team tab. 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. E-Commerce Currency: Choose the organisation's currency for e-commerce activity during registration or from Settings → Organization. The following rules apply:
    • Supported currencies: EUR, USD and GBP.
    • An organisation has one active e-commerce currency. Multi-currency selling and automatic currency conversion are not supported.
    • The administrator can change the currency until the first product or order is created. Creating the first product or order permanently locks the setting.
    • Products, orders, order items, fixed referral discounts, redemptions and payments automatically use and retain the organisation's commerce currency. Percentage referral discounts remain percentages and do not have a currency.
    • This setting applies only to e-commerce. It does not change subscriptions, administrative invoices, balances or other platform billing.
  3. 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 (support@checkmylicense.dev) 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 support@checkmylicense.dev. Configure SMTP only if you need emails to come from your own domain for branding purposes.

  4. API Keys: Manage your API authentication credentials for server-to-server integrations. You can create up to 5 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, provide a description (e.g., "Production API", "Development API"), and select the scopes that key is allowed to use.
    • Scoped – each key declares exactly what it is allowed to do via one or more scopes (see table below). Keys without a scope cannot call any protected endpoint.
    • 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.

    Available scopes:

    ScopeWhat it allows
    license:verifyVerify licenses (body-signed — see Encoding & Encryption in the API section)
    license:createCreate new licenses on behalf of the organisation
    license:updateUpdate existing license status, expiry, and access rules
    ecommerce:readRead customers and orders
    ecommerce:customers:writeCreate and update customers
    ecommerce:customers:verifyResolve existing checkout customers and verify OTPs
    ecommerce:orders:writeSubmit, update, and confirm orders
    ecommerce:orders:cancelCancel orders
    ecommerce:payments:recordRecord payments against orders
    ecommerce:provider-accounts:writeLook up and bind provider customer references
    ecommerce:payment-sessions:writeRegister and transition provider payment sessions
    ecommerce:provider-events:writeRecord and reconcile provider events
    Keep your API keys secure and do not share them publicly. Only active keys with the correct scope will authenticate successfully.
  5. Team: Growth and Pro administrators can open the Team tab and click Manage Members. Each member entry shows name, email, role, status, and invitation 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.

The administrator-only Email tab is available when the package contains White-labeled Emails. Team and Email settings remain hidden when the package does not include the corresponding capability.

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) – Core licensing for evaluation: 1 program, 2 active licences and 50 API hits per month.
  • Pay as you Go – No fixed platform fee; programs, active licences and API usage are metered from the first unit.
  • Growth – €39 monthly with 10 programs, 500 active licences, 20K API hits, Team Access and the Commerce & Automation Suite.
  • Pro – €110 monthly with 25 programs, 2,500 active licences, 250K API hits and priority support.

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 Operations → 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

All server-to-server API calls are authenticated with an API Key — a key/secret pair issued from Settings → Organization → Integrations. The secret is never transmitted directly; instead it is used to sign each request with HMAC-SHA256.

Base URL for all endpoints:

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

API Key Credentials

Each key pair consists of:

  • API Key — a public UUID sent in the X-API-Key header
  • API Secret — a shared secret used only to compute the request signature (never sent)

Each key declares one or more scopes that restrict which endpoints it can call:

ScopeGrants access to
license:verifyVerify licenses (body-signed — see Encoding & Encryption)
license:createCreate new licenses
license:updateUpdate existing licenses
ecommerce:readRead customers and orders
ecommerce:customers:writeCreate and update customers
ecommerce:customers:verifyResolve existing checkout customers and verify OTPs
ecommerce:orders:writeSubmit, update, and confirm orders
ecommerce:orders:cancelCancel orders
ecommerce:payments:recordRecord payments against orders
ecommerce:provider-accounts:writeLook up and bind provider customer references
ecommerce:payment-sessions:writeRegister and transition provider payment sessions
ecommerce:provider-events:writeRecord and reconcile provider events

A 403 Forbidden response with message Forbidden - scope '…' required means the key exists but lacks the necessary scope. Add the scope to the key in the portal.

Request Signing

Every request (except license/verify, which uses its own body-based scheme) must carry three headers computed as follows:

Signature algorithm
X-API-Signature = base64( HMAC-SHA256( api_secret, canonical_string ) )

canonical_string = api_key + "." + timestamp + "." + hex( SHA256( raw_body ) )
  • api_key — your API key UUID
  • timestamp — current Unix time in seconds (integer as string)
  • raw_body — the exact JSON string sent as the request body ({} for bodyless requests)

The server rejects requests where the timestamp differs from server time by more than 60 seconds. Ensure your server clock is synchronised.

Required Headers

HeaderValue
Content-Typeapplication/json
X-API-KeyYour API key UUID
X-API-TimestampUnix seconds (integer as string)
X-API-SignatureBase64 HMAC-SHA256 signature

Signing Helper (Node.js)

Build the three auth headers before every call:

buildApiKeyHeaders.js
import crypto from 'crypto';

/**
 * Build auth headers for every API call.
 * @param {string} apiKey    - Your API key UUID
 * @param {string} apiSecret - Your API secret
 * @param {string} bodyStr   - The raw JSON body string you will send (use '{}' if no body)
 * @returns {Object} Headers ready to pass to fetch / axios
 */
export function buildApiKeyHeaders(apiKey, apiSecret, bodyStr) {
  const ts        = String(Math.floor(Date.now() / 1000));
  const bodyHex   = crypto.createHash('sha256').update(bodyStr, 'utf8').digest('hex');
  const canonical = `${apiKey}.${ts}.${bodyHex}`;
  const sig       = crypto.createHmac('sha256', apiSecret)
                          .update(canonical)
                          .digest('base64');
  return {
    'Content-Type':    'application/json',
    'X-API-Key':       apiKey,
    'X-API-Timestamp': ts,
    'X-API-Signature': sig,
  };
}

Always stringify the body before calling buildApiKeyHeaders, then send the same string as the request body. Re-serialising independently produces a different hash and causes a 401.

Error Reference

HTTP StatusCause
400Invalid or missing request fields
401Missing headers, stale timestamp, or invalid signature
403API key inactive, or key does not have the required scope
404Requested resource not found
405Wrong HTTP method
409Duplicate provider_txn_id on payment record
500Unexpected server error

Common 401 / 403 messages

  • Unauthorized - missing API key headers — one or more of X-API-Key, X-API-Timestamp, X-API-Signature is absent
  • Unauthorized - stale timestamp — timestamp is more than 60 s old; check server clock sync
  • Unauthorized - invalid signature — verify the canonical string and that body bytes match exactly
  • Unauthorized - unknown API key — the UUID is not recognised
  • Forbidden - API key inactive — key exists but has been disabled
  • Forbidden - scope '…' required — key lacks the required scope

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
allowed_target_idsstring (comma-separated)body or querynoComma-separated list of allowed target IDs. If omitted or empty, all target IDs are permitted.
allowed_target_hostsstring (comma-separated)body or querynoComma-separated list of allowed target hosts. If omitted or empty, all hosts are permitted.
allowed_typesstring (comma-separated)body or querynoComma-separated list of allowed account types. If omitted or empty, all types are permitted.

Required scope: license:create

cURL Example
# Build the signature first (body is '{}' for query-param requests)
TS=$(date +%s)
BODY='{}'
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
CANONICAL="<api-key>.$TS.$BODY_HEX"
SIG=$(echo -n "$CANONICAL" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/create?email=user@example.com&program_name=My+Product&limit=3&expiry=2027-01-01" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d '{}'

License Restrictions (optional): Use allowed_target_ids, allowed_target_hosts, and allowed_types to restrict which activations are accepted for this license. Each is a comma-separated string. When a restriction list is provided and non-empty, only activations matching one of the listed values will be permitted during license verification. Omitting a restriction (or leaving it blank) means no constraint is applied for that field.

The response includes a restrictions object confirming what was saved:

{
  "target_ids": ["123456", "789012"],
  "target_hosts": ["MT5-Server\\Live"],
  "types": ["Live"]
}

Possible Responses

200License created

Success

{
  "success": {
    "message": "License created",
    "data": {
      "insert_licenses": {
        "affected_rows": 1,
        "returning": [{
          "id": 309,
          "email": "user@example.com",
          "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

POST/license/update

Update a license's fields including status and activation restrictions. Implemented in functions/license/update. The body must be JSON and must contain at minimum license_id.

ParameterTypeInRequiredDescription
license_idintegerbodyyesLicense ID to update
statusintegerbodyno0=active, 1=inactive (will set deactivated_at)
activations_deletablebooleanbodynoWhether activations can be deleted by the license holder
restrictionsobjectbodynoRestriction rules. Replaces the full set of restrictions for the license. Pass an empty array for any field to remove all restrictions of that type.
restrictions.target_idsstring[]bodynoAllowed target IDs. Empty array = no restriction.
restrictions.target_hostsstring[]bodynoAllowed target hosts. Empty array = no restriction.
restrictions.typesstring[]bodynoAllowed account types. Empty array = no restriction.

Required scope: license:update

cURL Example — Update status
BODY='{"license_id":124,"status":1}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/update" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"
cURL Example — Update restrictions
BODY='{"license_id":124,"restrictions":{"target_ids":["123456","789012"],"target_hosts":["MT5-Server\\Live"],"types":["Live"]}}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/update" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"

Restrictions behaviour: When a restrictions object is provided, the server replaces the entire restriction set — existing entries are deleted and the new ones are inserted. Pass an empty array ([]) for a field to remove all restrictions of that type.

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: "4", recommended). Use "4" for restriction enforcement. 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": "4"
  }'

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 activation.
200activation createdFirst time this target is verifying this license. New activation was created.
400missing required fieldsOne or more top-level required fields (api_id, ts, payload, sig) are missing or empty.
400unsupported protocol versionThe value passed in v is not a recognised protocol version. Use 4 (recommended), 3, or omit the field.
400invalid timestampThe ts field is not a valid numeric Unix timestamp.
400invalid payloadPayload cannot be decoded from base64 or is not valid JSON.
400target_id, target_host and type are required for v4When using protocol v4, the payload must include non-empty target_id, target_host, and type fields.
400program name required for v2/v3/v4The payload program field is required when using protocol version 2, 3, or 4.
401stale timestampTimestamp is too old or too far in the future (must be within ±60 seconds of server time).
401invalid api_idThe api_id was not found or the associated organization is inactive.
401invalid signatureHMAC-SHA256 signature verification failed. Verify your api_secret and signature computation.
403verification not allowedThe organization's subscription or plan does not permit license verification at this time.
403target type not allowedThe type value in the payload is not in the license's allowed types restriction list.
403target id not allowedThe target_id value in the payload is not in the license's allowed target IDs restriction list.
403target host not allowedThe target_host value in the payload is not in the license's allowed target hosts restriction list.
404unmatched licenseNo license matching the provided key exists for this organization.
400license inactiveLicense status is inactive/deactivated. Cannot verify an inactive license.
400invalid emailThe email in the payload does not match the license owner's email (comparison is case-insensitive).
400invalid programThe program name in the payload does not match the program associated with this license.
400expired certificateThe license expiry timestamp has passed. The license has expired.
400max limit reachedThe maximum number of activations for this license has been reached.
500error fetching licenseInternal error while querying the license record from the database.
500error fetching restrictionsInternal error while querying the license restriction tables (target IDs, hosts, or types).
500internal errorUnexpected internal 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": "4"
}

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: "4", recommended; 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

Creates a new customer record within the organization. Validates email format and uniqueness, phone number format (if provided), and enforces required fields (email, first_name, last_name). 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.typestringbodynoCustomer type. Must be "Individual" or "Business".
customer.postal_codestringbodynoPostal / ZIP code.
customer.country_codestringbodynoISO 3166-1 alpha-2 country code (e.g. "US", "DE", "GB"). Must be an active database reference value.
customer.statestringbodynoState, province, or region.
customer.addressstringbodynoFull street address.
customer.phonestringbodynoPhone number (international format supported). Must have at least 7 digits.
customer.tax_idstringbodynoTax identification number.
customer.refereebooleanbodynoWhether customer is a referee. Defaults to false.

Required scope: ecommerce:customers:write

Example Request
BODY=$(cat <<'EOF'
{
  "customer": {
    ...
  }
}
EOF
)
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/customer" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d '{
    "customer": {
      "email": "john.doe@example.com",
      "first_name": "John",
      "last_name": "Doe",
      "type": "Individual",
      "postal_code": "90001",
      "country_code": "US",
      "state": "California",
      "address": "123 Main Street, Apt 4B",
      "phone": "+1 (555) 123-4567",
      "tax_id": "US123456789",
      "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
  • type (optional): Must be Individual or Business if provided
  • phone (optional): 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",
      "type": "Individual",
      "postal_code": "90001",
      "country_code": "US",
      "state": "California",
      "address": "123 Main Street, Apt 4B",
      "phone": "+1 (555) 123-4567",
      "tax_id": "US123456789",
      "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.

Supports a preview mode — pass preview: true in the request body to compute pricing and referral discounts without persisting anything to the database.

Authenticated portal orders may omit order.sales_channel and are attributed to the protected Portal channel. Integrations should submit a configured channel slug when channel-specific catalog enforcement is required.

ParameterTypeInRequiredDescription
previewbooleanbodynoIf true, computes pricing and referral discount without creating any database records. Useful for previewing order totals before submission.
order.codestringbodynoCustom order code. Auto-generated if omitted.
order.customer_idintegerbodyyesID of the customer placing the order. Must belong to organization.
order.sales_channelstringbodynoStable sales-channel slug. Products and customer country are validated against this channel.
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.
order.items[].metadata.specific_rulesobjectbodynoOptional access rule to pre-populate on the licence generated from this item.
order.items[].metadata.specific_rules.typestringbodynoActivation type (e.g. "account", "machine"). Required when specific_rules is provided.
order.items[].metadata.specific_rules.target_idstringbodynoAccount number or machine ID to bind the generated licence to.
order.items[].metadata.specific_rules.target_hoststringbodynoHostname or broker server to restrict the licence to (e.g. "mt5.broker.com").

Required scope: ecommerce:orders:write

Example Request
BODY='{"order":{"code":"ORD2512290001ABC","customer_id":42,"sales_channel":"tradet","referral_code":"WELCOME10","items":[{"product_id":15,"qty":1,"metadata":{"specific_rules":{"type":"account","target_id":"12345","target_host":"mt5.broker.com"}}},{"product_id":18,"qty":2}]}}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/submit" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"
Preview Mode Example
BODY='{"preview":true,"order":{"customer_id":42,"referral_code":"WELCOME10","items":[{"product_id":15,"qty":1},{"product_id":18,"qty":2}]}}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/submit" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"

Preview Mode (preview: true):

  • Computes the full order total, discount, and referral application — identical to a real submission
  • No order, order items, or referral usage records are written to the database
  • Useful for showing the customer a live price breakdown before confirming

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"
  }
}

Sales Channel Catalog

Sales Channel Catalog

POST/sales-channel/catalog

Returns the saved, visible product catalog in channel order after applying active-channel and country restrictions. Products remain grouped by program_id; use the required, organization-unique program.code as the stable external join value.

ParameterTypeInRequiredDescription
channelstringbodyyesStable channel slug configured in the portal.
country_codestringbodynoISO 3166-1 alpha-2 code. Trusted edge geolocation is used when omitted.

Required scope: ecommerce:read

Request body
{
  "channel": "tradet",
  "country_code": "LB"
}
Allowed catalog
{
  "success": {
    "available": true,
    "country_code": "LB",
    "channel": { "slug": "tradet", "display_name": "Tradet" },
    "products": [
      {
        "program_id": 7,
        "program": { "code": "MT5_EA", "name": "Mitalio", "logo": "https://cdn.example/logo.png", "description": "Program description" },
        "product": { "code": "MT5_EA_ANNUAL", "title": "Annual", "description": "One-year licence" },
        "price": "99.00",
        "currency": "USD",
        "validity_days": 365,
        "activation_limit": 3,
        "activations_deletable": true,
        "is_default": true,
        "position": 0
      }
    ]
  }
}

A restricted country returns HTTP 200 with available: false and the configured fallback content. An unknown or inactive channel returns 404.

A channel may expose multiple products per program, but only one visible product in each program may be marked is_default. Never infer the default from title, price, validity, or position. Program codes lock permanently after external publication.

Get Order

Get Order

POST/order/get

Returns an organization-scoped order view using stored line-item snapshots rather than current product data.

ParameterTypeInRequiredDescription
order_codestringbodyyesStable ORD… code returned by order submission.

Required scope: ecommerce:read. The organization comes only from the API key or session.

Request body
{ "order_code": "ORD260720123456ABC" }
Order view
{
  "success": {
    "order": {
      "code": "ORD260720123456ABC",
      "status": 3,
      "sales_channel": { "slug": "tradet", "display_name": "Trade't" },
      "currency": "USD",
      "original_amount": "99.00",
      "discount_amount": "0.00",
      "final_amount": "99.00",
      "customer": { "name": "Mina Ali", "email": "mina@example.com", "country": "Lebanon", "country_code": "LB" },
      "items": [{
        "product_code": "MT5_EA_ANNUAL",
        "product_name": "Annual",
        "quantity": 1,
        "unit_price": "99.00",
        "unit_discount": "0.00",
        "unit_final_price": "99.00",
        "discount": "0.00",
        "subtotal": "99.00",
        "total": "99.00"
      }]
    }
  }
}

Cross-organization order views and payment attempts return the same ordinary 404 as an unknown code. Never send organization_id to this endpoint or /order/payment.

Trade't/Mitalio flow: catalogue, customer create/update, preview, submit, confirm, get, payment, then asynchronous licence generation. Use API-key authentication for every request and store program.code as cmlProgramCode.

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).
order.items[].metadata.specific_rulesobjectbodynoOptional access rule to pre-populate on the licence generated from this item.
order.items[].metadata.specific_rules.typestringbodynoActivation type (e.g. "account", "machine"). Required when specific_rules is provided.
order.items[].metadata.specific_rules.target_idstringbodynoAccount number or machine ID to bind the generated licence to.
order.items[].metadata.specific_rules.target_hoststringbodynoHostname or broker server to restrict the licence to (e.g. "mt5.broker.com").

Required scope: ecommerce:orders:write

Example Request
BODY='{"order":{"id":114,"code":"ORD2512290001XYZ","referral_code":"SAVE20","items":[{"product_id":15,"qty":5,"metadata":{"specific_rules":{"type":"account","target_id":"12345","target_host":"mt5.broker.com"}}},{"product_id":20,"qty":2}]}}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/update" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"

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).

Required scope: ecommerce:orders:write

Example Request (Primary format)
BODY='{"order_id":114}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/confirm" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"
Alternative Request Format
BODY='{"order":{"id":114}}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/confirm" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"

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).

Required scope: ecommerce:orders:cancel

Example Request (Simple Cancellation)
BODY='{"order_id":114,"notes":"Customer requested cancellation"}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/cancel" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"
Example Request (With Partial Refund and License Deactivation)
BODY='{"order_id":114,"refund":{"enabled":true,"amount":25.00},"deactivate_licenses":true,"notes":"Partial refund with license revocation"}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/cancel" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"

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.

Requires API-key or session authentication. The authenticated organization is always used to retrieve the order; cross-organization and unknown order codes both return 404.

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).

Required scope: ecommerce:payments:record

Example Request (Captured Payment)
BODY='{"payment":{"order_code":"ORD2512290001ABC","provider":"stripe","provider_txn_id":"pi_3AB12CD34EF56GH78","amount":53.98,"status":1,"payment_method":"card","external_reference":"cus_ABC123XYZ"}}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/payment" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"
Example Request (Refund)
BODY='{"payment":{"order_code":"ORD2512290001ABC","provider":"stripe","provider_txn_id":"re_3ZY98WX76VU54TS32","amount":25.00,"status":3,"external_reference":"refund-20251229"}}'
TS=$(date +%s)
BODY_HEX=$(echo -n "$BODY" | openssl dgst -sha256 | awk '{print $2}')
SIG=$(echo -n "<api-key>.$TS.$BODY_HEX" | openssl dgst -sha256 -hmac "<api-secret>" -binary | base64)

curl -X POST "https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/order/payment" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: <api-key>" \
  -H "X-API-Timestamp: $TS" \
  -H "X-API-Signature: $SIG" \
  -d "$BODY"

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
  • The authenticated organization is derived from the API key or session, never from the request body
  • 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

Add a secure, self-service activation page to your website so customers can view their licenses, review registered devices, and delete activations when the license permits it. You can build the flow directly with the API below or start from the open-source CML Activations Web SDK.

Start with the Web SDK

Fork or copy the CML Activations Web SDK, explore the live demo, and create a temporary test license through the CML Marketplace.

The test journey uses a real temporary license and an email you can access. It does not depend on a shared demo email or a fixed verification code.

Choose the Customer Access Scope

Configure the starter with one of these four access patterns. The organization and program values are public scope identifiers, not secrets.

Experiencemodeorganization_idprogram_idCustomer enters
Specific licenselicenseOmitOmitEmail + license
Organization-scoped licenselicenseRequiredOptionalEmail + license
Entire organizationorgRequiredOmitEmail
One program / EAorgRequiredRequiredEmail
Environment Configuration
NEXT_PUBLIC_CML_ACCESS_MODE=license
NEXT_PUBLIC_CML_ORGANIZATION_ID=
NEXT_PUBLIC_CML_PROGRAM_ID=

NEXT_PUBLIC_TURNSTILE_SITE_KEY=your-public-site-key
NEXT_PUBLIC_TURNSTILE_ACTION=cml-activation-access

# Optional CML activation widget identifier:
NEXT_PUBLIC_CML_ACTIVATION_WIDGET_ID=

In organization mode, the starter filters the returned license collection by program_id. Treat this as presentation scoping unless your CML API also enforces the program for organization sessions. Client-side filtering is not an authorization boundary.

Step 1: Render Turnstile and Request Access

POST/license/request-access

Render Cloudflare Turnstile with your public site key, collect a fresh token from its success callback, and send it with the customer's access details. CML validates the request and sends a temporary verification code to the customer's email.

Request Parameters

ParameterTypeInRequiredDescription
emailstringbodyyesCustomer email associated with the license or licenses
turnstileTokenstringbodyyesFresh token returned by the Cloudflare Turnstile success callback
modelicense | orgbodyyesWhether the customer opens one license or the organization-scoped collection
licensestringbodynoRequired when mode is license; omit when mode is org
organization_idstring (UUID)bodynoRequired in org mode; optional in license mode to constrain the supplied license to one organization
program_idintegerbodynoOptional program / EA scope used together with an organization ID
widget_keystringbodynoOptional CML activation widget identifier when your organization uses one; this is not the Cloudflare site key or secret
Example: Organization-scoped License
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": "<token-returned-by-turnstile>",
  "mode": "license",
  "organization_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
Example: Organization and Program
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",
  "turnstileToken": "<token-returned-by-turnstile>",
  "mode": "org",
  "organization_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "program_id": 123
}

Success Response

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

Turnstile requirements:

  • Put only the public Turnstile site key in NEXT_PUBLIC_TURNSTILE_SITE_KEY.
  • Never place the Cloudflare secret in a browser variable or public repository.
  • Disable submission until Turnstile returns a token.
  • Clear expired, timed-out, and failed tokens, then render a fresh widget after a failed request.
  • Add every local and production hostname to the widget's allowed hostnames in Cloudflare.

Step 2: Exchange the Email Code

POST/license/session

Ask the customer for the temporary code delivered by email. Exchange it for a short-lived session token and use that token as a Bearer token for activation-management calls.

Request Parameters

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

{
  "token": "<verification-code>"
}

Success Response

200 OK
{
  "success": {
    "session_token": "<short-lived-session-token>",
    "expires_at": "2026-07-27T12:15:00.000Z"
  }
}

The starter stores this short-lived token in sessionStorage. If you implement the flow through your own backend, an httpOnly secure cookie is preferable. Clear expired sessions and return the customer to the access form.

Step 3: List Activations

POST/license/activations

Retrieve the devices available to the authenticated session. License mode returns one license result, while organization mode returns a collection of licenses associated with the email in the configured organization.

Request Parameters

ParameterTypeInRequiredDescription
limitintegerbodyyesMaximum number of activation records to return
offsetintegerbodyyesNumber of activation records 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 <short-lived-session-token>

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

License-mode Response

200 OK — License Mode
{
  "success": {
    "activations": [
      {
        "id": 291,
        "license_id": 489,
        "target_id": "565443711",
        "target_host": "Home PC",
        "type": "Live",
        "created_at": "2026-07-27T10:30:00.000Z"
      }
    ],
    "aggregate_count": 1,
    "activations_deletable": true,
    "email": "john.doe@example.com",
    "license": "ee5e4a58dede4a2a9fcd66f9c282c99e",
    "program_name": "Example EA"
  }
}

Organization-mode Response

200 OK — Organization Mode
{
  "success": {
    "mode": "org",
    "email": "john.doe@example.com",
    "licenses": [
      {
        "id": 489,
        "license": "ee5e4a58dede4a2a9fcd66f9c282c99e",
        "email": "john.doe@example.com",
        "activations_deletable": true,
        "program_id": 123,
        "program": { "name": "Example EA" },
        "activations": [],
        "activations_aggregate": {
          "aggregate": { "count": 0 }
        }
      }
    ]
  }
}

Show delete controls only for a license whose activations_deletable value istrue. The API remains the authority and will reject deletion when it is not allowed.

Step 4: Delete Activation (Optional)

POST/license/activation-delete

Send the selected activation ID with the short-lived session token. Refresh the activation list after a successful deletion.

Request Parameters

ParameterTypeInRequiredDescription
activation_idintegerbodyyesID of the activation selected by the customer
Example Request
POST https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/activation-delete HTTP/1.1
Content-Type: application/json
Authorization: Bearer <short-lived-session-token>

{
  "activation_id": 291
}

Success Response

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

Deletion rules:

  • Only display delete buttons when activations_deletable is true.
  • Ask the customer to confirm before sending the deletion request.
  • Refresh the list after success so the page reflects the server state.
  • CML verifies session ownership and deletion permission server-side.

Test with a Temporary License

Use a temporary license rather than a shared demo account so you can experience the real email and verification flow end to end.

  1. Open the CML Marketplace.
  2. Complete a test order using an email address you can access.
  3. Wait for the temporary license email.
  4. Open the Activations demo.
  5. Enter the email and, when required by the selected mode, the issued license.
  6. Complete Turnstile, request the email code, and exchange it for an activation session.
  7. Review or delete activations when the license allows deletion.

Implementation and Security Notes

  • Use a fresh Turnstile token for every request-access attempt.
  • Organization IDs, program IDs, Turnstile site keys, and CML widget IDs are public identifiers, not secrets.
  • Never expose the Cloudflare Turnstile secret or any private API credential in client-side code.
  • Handle expired verification codes and session tokens by returning the customer to the correct step.
  • Do not treat a client-side program filter as an authorization boundary.
  • Log diagnostic details securely, but show customers concise messages that do not expose sensitive data.
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.

  • MQL4 and MQL5 SDKs (MetaTrader) – Supported for independently distributed MT4 and MT5 Expert Advisors. Use the package and example for the target platform, then verify the exact SDK and terminal build before release.
  • Python SDK v4 – ✅ Production-Ready - Command-line tool and library with offline tokens, program configuration, and HMAC-SHA256 authentication.
  • Node.js SDK – ✅ Production-Ready - Zero-dependency module using only Node built-ins; works with Node 18+ native fetch or falls back to the https module for older versions.

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

MQL4 and MQL5

The CML SDK supports license verification for independently distributed MetaTrader 4 and MetaTrader 5 Expert Advisors. Follow the package example for your target platform. Indicators require a different design, and WebRequest-based verification does not run in Strategy Tester.

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
  • An approved client credential model for the distributed build. Never embed an organization administrator or commerce secret in a client.
  • If the current SDK package requires a shared organization secret in the compiled program, pause release and request a technical integration assessment.

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

Begin with the documented MT5 Expert Advisor integration and verify its credential, cache, retry, and failure behavior before distribution:

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 v4

The Python SDK v4 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 v4 Support – Default v4 with program configuration; backward compatible with v1/v2/v3
  • 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 v4

  • License Restrictions – Server enforces target ID, target host, and account type restriction lists per license
  • Program Configuration – Server returns program-specific settings in response
  • Offline Tokens – Cached tokens include configuration for offline use (7-day validity)
  • Enhanced Security – v4 offline tokens include configuration in signature to prevent tampering
  • Version Selection – Choose protocol version with --v flag (default: 4)

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 v4 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 (v4)

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 4 \
  --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, 3, or 4 (default: 4)
--offlineNoUse cached offline token if available
--show-configNoDisplay program_configuration from response
--target-idNoTarget machine/device identifier (optional)

Protocol Versions

VersionFeatures
v4 (default)Includes program_configuration, offline_token with config in signature, and license restriction enforcement
v3Includes 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. v4 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 v4 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=4
    )
    
    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=4
)

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=4
    )
    
    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

v4 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.

Node.js SDK

The Node.js SDK provides a zero-dependency module for integrating CheckMyLicense verification into Node.js applications. It uses only Node built-ins and supports both modern and legacy Node versions, with built-in offline token caching and program configuration access.

Key Features

  • Zero Dependencies – Uses only Node built-ins: crypto, fs, https, os, path
  • Broad Compatibility – Uses Node 18+ native fetch or falls back to https module for older versions
  • Protocol v4 Support – Program configuration, offline tokens, and license restriction enforcement
  • Offline Token Caching – Automatic caching for offline verification without network access
  • Server Signature Verification – Built-in validation of server responses
  • CommonJS Module – Simple require() integration with no build step needed

What's New in v4

  • License Restrictions – Server enforces target ID, target host, and account type restriction lists per license
  • Program Configuration – Server returns program-specific settings in the response
  • Offline Tokens – Cached tokens include configuration for offline use (7-day validity)
  • Enhanced Security – v4 offline tokens include configuration in signature to prevent tampering

Installation

Prerequisites

  • Node.js 14 or higher (Node 18+ recommended for native fetch)
  • Active CheckMyLicense organization account
  • Your organization's API ID and API Secret (from Settings → Organization)

Installation Steps

  1. Download the SDK

    Download the Node.js SDK from your organization page (Available SDK section).

  2. Extract the File

    The SDK package contains a single file:

    • CheckMyLicenseOnline.js – The SDK module
  3. Place in Your Project

    Copy CheckMyLicenseOnline.js into your project directory, then require it directly — nonpm install needed.

  4. Verify Installation
    node -e "const { CheckMyLicenseOnline } = require('./CheckMyLicenseOnline'); console.log('SDK loaded');"

Offline Token Storage: Offline tokens are automatically cached to a .cml_offline/ folder in the user's home directory after successful online verification. This directory is created automatically if it doesn't exist.

Quick Start

Basic Online Verification (v4)

Verify a license and retrieve program configuration in a single call:

const { CheckMyLicenseOnline } = require('./CheckMyLicenseOnline');

const result = await CheckMyLicenseOnline({
  apiId:    'your-api-id',
  apiSecret: 'your-api-secret',
  endpoint: 'https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1',
  license:  'user-license-key',
  email:    'user@example.com',
  program:  'My Node Application',
  version:  '4',
});

if (result.success) {
  console.log('License verified:', result.message);
  console.log('Source:', result.source); // 'online' | 'offline'
  console.log('Config:', result.programConfiguration);
} else {
  console.error('Verification failed:', result.message);
  process.exit(1);
}

Offline Verification

Use a cached offline token when no network access is available:

const result = await CheckMyLicenseOnline({
  program: 'My Node Application',
  license: 'user-license-key',
  offline: true,
});

console.log('Source:', result.source); // 'offline'
console.log('Config:', result.programConfiguration);

Function Parameters

ParameterRequiredDescription
apiIdOnline onlyOrganization API ID from Settings → Organization
apiSecretOnline onlyOrganization API Secret from Settings → Organization
endpointOnline onlyCheckMyLicense API endpoint URL
licenseYesLicense key to verify
emailOnline onlyLicense owner's email address
programYesProgram name (must match CheckMyLicense dashboard)
targetIdNoTarget machine/device identifier
targetHostNoTarget host identifier
typeNoAccount type identifier
versionNoProtocol version: '1', '2', '3', or '4' (default: '4')
offlineNoSet true to use cached offline token if available
verifyServerSigNoSet true to validate the server's response signature

Return Value

PropertyTypeDescription
successbooleanWhether verification succeeded
messagestringHuman-readable result or error description
source'online' | 'offline'Whether the result came from the server or cached token
dataobjectFull parsed server response data
programConfigurationobjectProgram-specific configuration returned by the server

Offline Token Behavior: Offline tokens are valid for 7 days from issuance. After the first successful online verification, subsequent calls can pass offline: true for instant verification without network access.

Examples

Offline-First with Online Fallback

Try the cached offline token first and only contact the server if no valid token exists:

const { CheckMyLicenseOnline } = require('./CheckMyLicenseOnline');

async function verifyLicense(licenseKey, email) {
  // Attempt offline first
  const offline = await CheckMyLicenseOnline({
    program: 'My Node Application',
    license: licenseKey,
    offline: true,
  });

  if (offline.success) {
    console.log('Verified offline');
    return offline;
  }

  // Fall back to online
  console.log('Offline token unavailable, verifying online...');
  return await CheckMyLicenseOnline({
    apiId:     process.env.CML_API_ID,
    apiSecret: process.env.CML_API_SECRET,
    endpoint:  process.env.CML_ENDPOINT,
    license:   licenseKey,
    email,
    program:   'My Node Application',
    version:   '4',
    verifyServerSig: true,
  });
}

const result = await verifyLicense('user-license-key', 'user@example.com');
if (!result.success) {
  console.error(result.message);
  process.exit(1);
}
console.log('Access granted. Config:', result.programConfiguration);

Configuration-Driven Features

Use programConfiguration to gate features at runtime:

const { CheckMyLicenseOnline } = require('./CheckMyLicenseOnline');

async function startApp(licenseKey, email) {
  const result = await CheckMyLicenseOnline({
    apiId:     process.env.CML_API_ID,
    apiSecret: process.env.CML_API_SECRET,
    endpoint:  process.env.CML_ENDPOINT,
    license:   licenseKey,
    email,
    program:   'Professional Node App',
    version:   '4',
  });

  if (!result.success) {
    throw new Error(`License invalid: ${result.message}`);
  }

  const config = result.programConfiguration ?? {};

  return {
    maxUsers:        config.max_users        ?? 1,
    advancedFeatures: config.advanced_features ?? false,
    apiRateLimit:    config.api_rate_limit    ?? 100,
  };
}

const appConfig = await startApp(userLicense, userEmail);
console.log('App config:', appConfig);

if (appConfig.advancedFeatures) {
  enableAdvancedMode();
}

Express.js Middleware Example

Protect API endpoints with license verification middleware:

const express = require('express');
const { CheckMyLicenseOnline } = require('./CheckMyLicenseOnline');

const app = express();
app.use(express.json());

async function licenseMiddleware(req, res, next) {
  const { license, email } = req.headers;

  if (!license || !email) {
    return res.status(401).json({ error: 'License and email headers required' });
  }

  const result = await CheckMyLicenseOnline({
    apiId:     process.env.CML_API_ID,
    apiSecret: process.env.CML_API_SECRET,
    endpoint:  process.env.CML_ENDPOINT,
    license,
    email,
    program:   'My API Service',
    version:   '4',
  });

  if (!result.success) {
    return res.status(403).json({ error: result.message });
  }

  req.licenseConfig = result.programConfiguration ?? {};
  next();
}

app.get('/api/data', licenseMiddleware, (req, res) => {
  const limit = req.licenseConfig.api_rate_limit ?? 100;
  res.json({ data: 'protected content', rateLimit: limit });
});

app.listen(3000);

Response Data Structure

v4 Success Response

{
  success: true,
  message: 'License verified successfully',
  source: 'online',
  data: {
    status: 'success',
    validity: 1735689600,
    limit: 5,
    used: 2,
    offline_token: 'eyJwcm9ncmF...',
    offline_expires_at: 1739462400
  },
  programConfiguration: {
    max_users: 5,
    advanced_features: true,
    api_rate_limit: 1000
  }
}

Failure Response

{
  success: false,
  message: 'License not found or expired',
  source: 'online',
  data: null,
  programConfiguration: null
}

Best Practices: Store API credentials in environment variables — never hardcode them. Use ?? (nullish coalescing) with defaults when readingprogramConfiguration to gracefully handle programs with no configuration set. For long-running services, implement periodic re-verification (e.g., every 24 hours).

Security Note: v4 offline tokens include the program configuration in the cryptographic signature. If the cached configuration is tampered with, signature verification will fail and the SDK will require a fresh online check.

Developer Documentation — CheckMyLicense