Documentation
Learn how to use CheckMyLicense with step-by-step guides, API reference, and SDK examples.
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.
- Go to Programs and click Add Program.
- Enter a Program Name and Program Code (unique short identifier). Optionally upload a logo and write a description.
- Optionally add Configuration parameters (key-value pairs) that will be included in license verification responses.
- 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.
- 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:
- 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.
- Marks the version as Released — the status badge updates immediately in both the modal and the versions table.
- 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_versionparameter (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_featureto require users to complete a certain number of actions before unlocking premium features. - Runtime Limits: Control parameters like
max_accounts,max_symbols, orsession_timeoutto enforce usage limits. - Feature Flags: Enable or disable specific features using boolean flags like
allow_backtestingorenable_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.
- Navigate to Licences and click Add Licence.
- Select the Program, type the Customer Email, set a Limit (devices allowed, 0 = unlimited) and an Expiry Date (or leave blank for unlimited).
- 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.
liveordemo. 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=licensewithout an organization or program asks for email and license.mode=orgwith an organization asks for email and shows licenses across that organization.mode=orgwith an organization and program asks for email and presents licenses for that program.mode=licensewith 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
- Log in to the Cloudflare dashboard and navigate to Turnstile in the left sidebar.
- Click Add widget, give it a name, and enter the domain(s) where it will be used.
- Choose a widget type (Managed is recommended) and click Create.
- Copy the Site Key (public) and the Secret Key — you will need both in the next step.
Step 2 — Register the widget in CheckMyLicense
- Go to Organisation (under Settings in the left navigation).
- Scroll down to the Turnstile Widgets section.
- 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.
- 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
- Navigate to Products and select Add Product.
- 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.
- Mark Discountable if referral codes should apply discounts.
- Define Limit (number of licences per purchase, 0 = unlimited) and Validity (days before licence expiry, 0 = no expiry).
- 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.
- Write a description for storefront display.
- 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.
- Open Sales Channels under E-Commerce and select Create channel.
- Set the channel identity and public links. Keep a new channel inactive while configuring it.
- Choose All countries or select allowed countries from the database-backed continent groups.
- 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.
- 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
- Navigate to Whitelist (under E‑Commerce) and click Add Whitelist.
- Enter a Name for the whitelist (e.g. “Live Accounts Only” or “VIP Servers”).
- 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.
- Click Add Item (or the inline form above the table).
- Fill in any combination of the three criteria:
- Type — activation type, e.g.
liveordemo. 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.
- Type — activation type, e.g.
- Click the check button to save the item. It appears in the table immediately.
- Use the toggle icon to activate or deactivate individual items without deleting them.
- 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
- Choose Excel file and click Download Excel template. Always use the downloaded template rather than changing its columns.
- Enter one item per row. target_id is required; type, target_host and active are optional. Set active to
trueorfalse. - Keep the workbook as a single
.xlsxworksheet no larger than 2 MB. The required columns aretarget_id,type,target_hostandactive. Use plain values only; formulas are not accepted. - 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
- Choose Quick list, then select whether your pasted values represent Target ID, Type or Target host / Server ID.
- Paste up to 50 comma-separated or newline-separated values.
- Quick List creates missing values only. A value already present in the whitelist remains unchanged.
- 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
- Click Review preview and verify the displayed rows and any wildcard warning.
- Check the confirmation box, then click Confirm and run.
- Review the summary of inserted, updated, unchanged and rejected rows. Click Download results for the complete row-by-row report.
- 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:
- Open the Inactive Licenses tab.
- Check the rows whose Target ID / Target Host you want to add. Use the header checkbox to select all visible rows at once.
- Click Add to List (N) — a modal appears listing all your whitelists.
- Select the target whitelist from the dropdown and click Add to Whitelist.
- 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.
- 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
- Navigate to Referees and click Add Referee.
- Provide the contact's Email, First Name and Last Name (all required).
- Optionally select the Type (Individual or Business), enter the Postal Code, Country, Phone, State, Address and Tax ID.
- Click Save.
- 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.
- Go to Operations → Customers and click Add Customer.
- Enter the Email, First Name and Last Name (all required).
- Optionally select the Type (Individual or Business), enter the Postal Code, Country, Phone, State, Address and Tax ID. Click Create.
- 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
- Navigate to Orders and click Create Order. Select a Customer. Optionally choose a Referral Code; the discount value is calculated automatically.
- 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.
- 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.
- Click Create Order. A confirmation modal shows the order code and total. Confirming sets the status to Draft. Open the order details to review.
- 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
- Go to Payments and click Add Payment.
- 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
- Order Code – e.g.,
- 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
- Organization Information: Edit fields such as organisation name, type (Personal or Company), phone, email, country, website, Tax ID and billing address.
- 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.
- 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.
- 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:
Keep your API keys secure and do not share them publicly. Only active keys with the correct scope will authenticate successfully.Scope What 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 - 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
- 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.
- Check the box agreeing that credits will pay current and future dues.
- 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:
- Create a Program and add a version under Operations → Programs.
- Create a Product linked to that program under E‑Commerce → Products.
- (Optional) Add a Referee and Referral Code if you wish to offer discounts.
- Add a Customer under Operations → Customers.
- Create an Order for the customer in Orders. Select the product, apply any referral code and confirm the order. The order enters Draft status.
- Confirm the Order via the blue check icon on the order page. The status becomes Pending Payment.
- 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).
- 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.
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/v1API Key Credentials
Each key pair consists of:
API Key— a public UUID sent in theX-API-KeyheaderAPI 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:
| Scope | Grants access to |
|---|---|
license:verify | Verify licenses (body-signed — see Encoding & Encryption) |
license:create | Create new licenses |
license:update | Update existing licenses |
ecommerce:read | Read customers and orders |
ecommerce:customers:write | Create and update customers |
ecommerce:customers:verify | Resolve existing checkout customers and verify OTPs |
ecommerce:orders:write | Submit, update, and confirm orders |
ecommerce:orders:cancel | Cancel orders |
ecommerce:payments:record | Record payments against orders |
ecommerce:provider-accounts:write | Look up and bind provider customer references |
ecommerce:payment-sessions:write | Register and transition provider payment sessions |
ecommerce:provider-events:write | Record 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:
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
| Header | Value |
|---|---|
Content-Type | application/json |
X-API-Key | Your API key UUID |
X-API-Timestamp | Unix seconds (integer as string) |
X-API-Signature | Base64 HMAC-SHA256 signature |
Signing Helper (Node.js)
Build the three auth headers before every call:
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 Status | Cause |
|---|---|
400 | Invalid or missing request fields |
401 | Missing headers, stale timestamp, or invalid signature |
403 | API key inactive, or key does not have the required scope |
404 | Requested resource not found |
405 | Wrong HTTP method |
409 | Duplicate provider_txn_id on payment record |
500 | Unexpected server error |
Common 401 / 403 messages
Unauthorized - missing API key headers— one or more ofX-API-Key,X-API-Timestamp,X-API-Signatureis absentUnauthorized - stale timestamp— timestamp is more than 60 s old; check server clock syncUnauthorized - invalid signature— verify the canonical string and that body bytes match exactlyUnauthorized - unknown API key— the UUID is not recognisedForbidden - API key inactive— key exists but has been disabledForbidden - scope '…' required— key lacks the required scope
Create License
Create License
/license/createCreate a new license. Implemented in functions/license/create. The endpoint requires organization context (auth provides organization_id).
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
email | string | body or query | yes | License owner email |
program_name | string | body or query | yes | Program name |
limit | integer | body or query | no | Activation limit |
expiry | bigint | body or query | no | Expiry timestamp (seconds or ms accepted) |
activations_deletable | boolean | body or query | no | Whether activations can be deleted |
allowed_target_ids | string (comma-separated) | body or query | no | Comma-separated list of allowed target IDs. If omitted or empty, all target IDs are permitted. |
allowed_target_hosts | string (comma-separated) | body or query | no | Comma-separated list of allowed target hosts. If omitted or empty, all hosts are permitted. |
allowed_types | string (comma-separated) | body or query | no | Comma-separated list of allowed account types. If omitted or empty, all types are permitted. |
Required scope: license:create
# 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
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"
}
}
}
}Missing or invalid parameters
{
"error": {
"message": "Invalid license request",
"errors": ["email is required", "program_name not found"]
}
}Caller is not associated with an organization
{
"error": {
"message": "User must belong to an organization to create licenses"
}
}Program not owned by organization or not found
{
"error": {
"message": "Program not found or not owned by this organization"
}
}Subscription does not allow creating more licenses
{
"error": {
"message": "Subscription limits prevent creating new licenses",
"reason": "license limit reached for current package"
}
}General failure while processing the request
{
"error": {
"message": "Error creating license",
"data": { "message": "<error object>", "stack": "..." }
}
}Update License
Update License
/license/updateUpdate 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
license_id | integer | body | yes | License ID to update |
status | integer | body | no | 0=active, 1=inactive (will set deactivated_at) |
activations_deletable | boolean | body | no | Whether activations can be deleted by the license holder |
restrictions | object | body | no | Restriction 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_ids | string[] | body | no | Allowed target IDs. Empty array = no restriction. |
restrictions.target_hosts | string[] | body | no | Allowed target hosts. Empty array = no restriction. |
restrictions.types | string[] | body | no | Allowed account types. Empty array = no restriction. |
Required scope: license:update
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"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.
{
"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
/license/verifyVerify a license and create an activation record using HMAC-SHA256 signature authentication. Implemented in functions/license/verify.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
api_id | string (UUID) | body | yes | Organization ID (from Settings → Organization) |
ts | string | body | yes | Current Unix timestamp in seconds (UTC) |
payload | string (base64) | body | yes | Base64-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. |
sig | string (base64) | body | yes | HMAC-SHA256 signature of canonical string (base64 encoded) |
v | string | body | no | API version (default: "4", recommended). Use "4" for restriction enforcement. Versions "1" and "2" are legacy and no longer supported. |
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": "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 keyemail: License owner's email address (must match the license, comparison is case-insensitive)target_id: Account number or unique machine identifiertarget_host: Server name or hostnametype: 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
| Status | Message | Description |
|---|---|---|
200 | activation exists | License verified successfully. This target already has an active activation. |
200 | activation created | First time this target is verifying this license. New activation was created. |
400 | missing required fields | One or more top-level required fields (api_id, ts, payload, sig) are missing or empty. |
400 | unsupported protocol version | The value passed in v is not a recognised protocol version. Use 4 (recommended), 3, or omit the field. |
400 | invalid timestamp | The ts field is not a valid numeric Unix timestamp. |
400 | invalid payload | Payload cannot be decoded from base64 or is not valid JSON. |
400 | target_id, target_host and type are required for v4 | When using protocol v4, the payload must include non-empty target_id, target_host, and type fields. |
400 | program name required for v2/v3/v4 | The payload program field is required when using protocol version 2, 3, or 4. |
401 | stale timestamp | Timestamp is too old or too far in the future (must be within ±60 seconds of server time). |
401 | invalid api_id | The api_id was not found or the associated organization is inactive. |
401 | invalid signature | HMAC-SHA256 signature verification failed. Verify your api_secret and signature computation. |
403 | verification not allowed | The organization's subscription or plan does not permit license verification at this time. |
403 | target type not allowed | The type value in the payload is not in the license's allowed types restriction list. |
403 | target id not allowed | The target_id value in the payload is not in the license's allowed target IDs restriction list. |
403 | target host not allowed | The target_host value in the payload is not in the license's allowed target hosts restriction list. |
404 | unmatched license | No license matching the provided key exists for this organization. |
400 | license inactive | License status is inactive/deactivated. Cannot verify an inactive license. |
400 | invalid email | The email in the payload does not match the license owner's email (comparison is case-insensitive). |
400 | invalid program | The program name in the payload does not match the program associated with this license. |
400 | expired certificate | The license expiry timestamp has passed. The license has expired. |
400 | max limit reached | The maximum number of activations for this license has been reached. |
500 | error fetching license | Internal error while querying the license record from the database. |
500 | error fetching restrictions | Internal error while querying the license restriction tables (target IDs, hosts, or types). |
500 | internal error | Unexpected 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_idand the license'slast_used_atis updated. - Activation insertion is best-effort; verification succeeds even if activation DB insert fails.
- If an activation with matching
target_id,target_host, andtypealready 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:
- Prepare JSON payload with license, email, and target info
- Base64 encode the JSON payload
- Get current Unix timestamp (seconds, UTC)
- Build canonical string exactly as:
api_id.ts.payload_base64— wherepayload_base64is the literal base64 string you include in the payload field. Do NOT decode or alter the base64 string when creating the canonical string. - Compute HMAC-SHA256 of canonical string using your
api_secret - Base64 encode the HMAC result
- Send JSON request with api_id, ts, payload, sig, and v fields
Building the Payload
The JSON payload contains the license and activation information:
{
"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 keyemail: Email address of the license ownertarget_id: Account login number or unique machine IDtarget_host: Server hostname or terminal identifiertype: Account type (e.g., "Demo", "Live")program: Program name (must match the license's program)
eyJsaWNlbnNlIjoiOTg0NWNjYTk4YmJlNDY2NGJiYmM5ZmIxMDg2MmE0NTMiLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJ0YXJnZXRfaWQiOiIxMjM0NTY3ODkiLCJ0YXJnZXRfaG9zdCI6Ik1UNS1EZXNrdG9wXFxFVVJVU0QiLCJ0eXBlIjoiTGl2ZSIsInByb2dyYW0iOiJNeSBQcm9kdWN0In0=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:
- Take the canonical string:
api_id.ts.payload_base64 - Compute HMAC-SHA256 using your
api_secretas the key and the canonical string as the data - Base64 encode the resulting HMAC digest
- Use this base64-encoded value as the
sigparameter
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:
${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.
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
{
"api_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"ts": "1729875123",
"payload": "eyJsaWNlbnNlIjoiOTg0NWNjYTk4YmJlNDY2NGJiYmM5ZmIxMDg2MmE0NTMiLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJ0YXJnZXRfaWQiOiIxMjM0NTY3ODkiLCJ0YXJnZXRfaG9zdCI6Ik1UNS1EZXNrdG9wXFxcXEVVUlVTRCIsInR5cGUiOiJMaXZlIiwicHJvZ3JhbSI6Ik15IFByb2R1Y3QifQ==",
"sig": "BASE64_HMAC_SIGNATURE",
"v": "4"
}Breaking it down:
api_id= Organization IDts= 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, anddatawith validity/limit/used. - 400-500 — Error. Response contains
errorobject withcodeandmessage.
{
"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 activationsprogram_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 containingvalidity,limit,used, andconfigurationfields (optional — generated best-effort). When decoded, the offline token includes aconfigurationfield (notprogram_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": {
"code": 401,
"message": "invalid signature"
}
}Signature Verification Failed?
- Verify your
api_secretis 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
sigfield
Create Customer
Create Customer
/order/customerCreates 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
customer.email | string | body | yes | Customer email address. Must be valid format and unique within organization. |
customer.first_name | string | body | yes | Customer first name. Cannot be empty. |
customer.last_name | string | body | yes | Customer last name. Cannot be empty. |
customer.type | string | body | no | Customer type. Must be "Individual" or "Business". |
customer.postal_code | string | body | no | Postal / ZIP code. |
customer.country_code | string | body | no | ISO 3166-1 alpha-2 country code (e.g. "US", "DE", "GB"). Must be an active database reference value. |
customer.state | string | body | no | State, province, or region. |
customer.address | string | body | no | Full street address. |
customer.phone | string | body | no | Phone number (international format supported). Must have at least 7 digits. |
customer.tax_id | string | body | no | Tax identification number. |
customer.referee | boolean | body | no | Whether customer is a referee. Defaults to false. |
Required scope: ecommerce:customers:write
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
IndividualorBusinessif provided - phone (optional): Must contain at least 7 digits; allows
+,-, spaces, parentheses
Possible Responses
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"
}
}
}Missing required fields, invalid email format, invalid phone format, or duplicate email
{
"error": {
"code": 400,
"message": "Invalid email format"
}
}Missing or invalid JWT token
{
"error": {
"code": 401,
"message": "Authentication required"
}
}User lacks organization membership
{
"error": {
"code": 403,
"message": "Organization membership required"
}
}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_idandorganization_idare automatically set from JWT context
Submit Order
Submit Order
/order/submitCreates 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
preview | boolean | body | no | If true, computes pricing and referral discount without creating any database records. Useful for previewing order totals before submission. |
order.code | string | body | no | Custom order code. Auto-generated if omitted. |
order.customer_id | integer | body | yes | ID of the customer placing the order. Must belong to organization. |
order.sales_channel | string | body | no | Stable sales-channel slug. Products and customer country are validated against this channel. |
order.referral_code | string | body | no | Referral/discount code to apply. |
order.items | array | body | yes | Array of line items with product_id and qty. |
order.items[].product_id | integer | body | yes | Product ID. Must belong to organization. |
order.items[].qty | integer | body | yes | Quantity (positive integer). Duplicates are consolidated. |
order.items[].metadata.specific_rules | object | body | no | Optional access rule to pre-populate on the licence generated from this item. |
order.items[].metadata.specific_rules.type | string | body | no | Activation type (e.g. "account", "machine"). Required when specific_rules is provided. |
order.items[].metadata.specific_rules.target_id | string | body | no | Account number or machine ID to bind the generated licence to. |
order.items[].metadata.specific_rules.target_host | string | body | no | Hostname or broker server to restrict the licence to (e.g. "mt5.broker.com"). |
Required scope: ecommerce:orders:write
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"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
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": [...]
}
}
}Invalid customer_id, missing items, invalid product prices, or duplicate order code
{
"error": {
"code": 400,
"message": "Customer not found in organization"
}
}Customer or products not found in organization
{
"error": {
"code": 404,
"message": "Product not found"
}
}Sales Channel Catalog
Sales Channel Catalog
/sales-channel/catalogReturns 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
channel | string | body | yes | Stable channel slug configured in the portal. |
country_code | string | body | no | ISO 3166-1 alpha-2 code. Trusted edge geolocation is used when omitted. |
Required scope: ecommerce:read
{
"channel": "tradet",
"country_code": "LB"
}{
"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
/order/getReturns an organization-scoped order view using stored line-item snapshots rather than current product data.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
order_code | string | body | yes | Stable ORD… code returned by order submission. |
Required scope: ecommerce:read. The organization comes only from the API key or session.
{ "order_code": "ORD260720123456ABC" }{
"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
/order/updateUpdates 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
order.id | integer | body | yes | ID of the order to update. Must be in draft status (0). |
order.code | string | body | no | New order code (optional). Must be unique within organization. |
order.referral_code | string | body | no | New referral code. null removes existing referral. |
order.items | array | body | yes | Complete replacement item list. Previous items are deleted. |
order.items[].product_id | integer | body | yes | Product ID. Must belong to organization. |
order.items[].qty | integer | body | yes | Quantity (positive integer). |
order.items[].metadata.specific_rules | object | body | no | Optional access rule to pre-populate on the licence generated from this item. |
order.items[].metadata.specific_rules.type | string | body | no | Activation type (e.g. "account", "machine"). Required when specific_rules is provided. |
order.items[].metadata.specific_rules.target_id | string | body | no | Account number or machine ID to bind the generated licence to. |
order.items[].metadata.specific_rules.target_host | string | body | no | Hostname or broker server to restrict the licence to (e.g. "mt5.broker.com"). |
Required scope: ecommerce:orders:write
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
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": [...]
}
}
}Invalid order id, non-draft status, missing items, duplicate code, or invalid products
{
"error": {
"code": 400,
"message": "Order is not in draft status"
}
}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_attimestamp is automatically set
Confirm Order
Confirm Order
/order/confirmTransitions 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
order_id | integer | body | yes | ID of the order to confirm. Must be in draft status (0). |
Required scope: ecommerce:orders:write
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"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
| Status | Label | Description |
|---|---|---|
0 | Draft | Order created but not confirmed |
1 | Pending Payment | Order confirmed, awaiting payment |
2 | Partially Paid | Some payment received |
3 | Paid | Fully paid |
4 | Cancelled | Order cancelled |
5 | Refunded | Payment refunded |
6 | Partially Refunded | Partial refund issued |
Possible Responses
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"
}
}
}Invalid order_id or order not in draft status
{
"error": {
"code": 400,
"message": "Order is not in draft status"
}
}Order not found in organization
{
"error": {
"code": 404,
"message": "Order not found"
}
}Cancel Order
Cancel Order
/order/cancelCancels 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
order_id | integer | body | yes | ID of the order to cancel. |
refund | object | body | no | Refund configuration. Omit for no refund. |
refund.enabled | boolean | body | no | Set to true to create refund payment. |
refund.amount | number | body | no | Specific refund amount. Omit for full auto-calculated refund. |
deactivate_licenses | boolean | body | no | Set to true to queue license deactivation job. |
notes | string | body | no | Cancellation notes (stored in order.notes). |
Required scope: ecommerce:orders:cancel
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"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
| Scenario | refund.enabled | deactivate_licenses | Behavior |
|---|---|---|---|
| Simple Cancel | false or omitted | false or omitted | Sets status to 4, updates canceled_at |
| Cancel with Full Refund | true, no amount | false or omitted | Cancels + creates refund for all captured funds |
| Cancel with Partial Refund | true, with amount | false or omitted | Cancels + creates refund for specified amount |
| Cancel with License Revocation | false or omitted | true | Cancels + queues license deactivation job |
| Full Cancel Package | true | true | Cancels + refunds + queues license deactivation |
Possible Responses
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
}
}
}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
}
}
}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_amountand available captured funds - License deactivation is asynchronous (job-based) and non-blocking
- Multiple cancellations are idempotent when no actions are requested
canceled_attimestamp is set on first cancellation only
Record Payment
Record Payment
/order/paymentRecords 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.
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
payment.order_code | string | body | yes | Order code to apply payment to. |
payment.provider | string | body | yes | Payment provider name (e.g., "stripe", "paypal", "manual"). |
payment.provider_txn_id | string | body | yes | Unique transaction ID from payment provider. |
payment.amount | number/string | body | yes | Payment amount (positive number). Accepts numeric strings. |
payment.status | integer | body | yes | Payment status: 1 = captured, 2 = failed, 3 = refund. |
payment.payment_method | string | body | no | Payment method type (e.g., "card", "bank_transfer"). |
payment.external_reference | string | body | no | Additional reference ID (e.g., customer ID, invoice number). |
Required scope: ecommerce:payments:record
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"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
| Status | Label | Description | Effect on Order |
|---|---|---|---|
1 | Captured | Payment successfully received | Adds to captured total; updates order status when fully paid |
2 | Failed | Payment attempt failed | Recorded for audit; no effect on order status |
3 | Refund | Money returned to customer | Recorded; does NOT reduce captured total per spec |
Order Status Updates
| Scenario | Order Status | Description |
|---|---|---|
| First captured payment < final_amount | 2 (Partially Paid) | Some payment received |
| Captured total >= final_amount | 3 (Paid) | Fully paid; paid_at timestamp set |
| Failed payment | No change | Status remains as-is |
| Refund payment | No change | Use /order/cancel with refund for status updates |
Possible Responses
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"
}
}
}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
}
}
}Missing required fields, invalid status, invalid amount, or duplicate provider_txn_id
{
"error": {
"code": 400,
"message": "Invalid payment status"
}
}Order code not found in organization
{
"error": {
"code": 404,
"message": "Order not found"
}
}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_idmust 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/cancelendpoint 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.
| Experience | mode | organization_id | program_id | Customer enters |
|---|---|---|---|---|
| Specific license | license | Omit | Omit | Email + license |
| Organization-scoped license | license | Required | Optional | Email + license |
| Entire organization | org | Required | Omit | |
| One program / EA | org | Required | Required |
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
/license/request-accessRender 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
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
email | string | body | yes | Customer email associated with the license or licenses |
turnstileToken | string | body | yes | Fresh token returned by the Cloudflare Turnstile success callback |
mode | license | org | body | yes | Whether the customer opens one license or the organization-scoped collection |
license | string | body | no | Required when mode is license; omit when mode is org |
organization_id | string (UUID) | body | no | Required in org mode; optional in license mode to constrain the supplied license to one organization |
program_id | integer | body | no | Optional program / EA scope used together with an organization ID |
widget_key | string | body | no | Optional CML activation widget identifier when your organization uses one; this is not the Cloudflare site key or secret |
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"
}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
{
"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
/license/sessionAsk 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
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
token | string | body | yes | Temporary verification code sent to the customer email |
POST https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1/license/session HTTP/1.1
Content-Type: application/json
{
"token": "<verification-code>"
}Success Response
{
"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
/license/activationsRetrieve 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
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
limit | integer | body | yes | Maximum number of activation records to return |
offset | integer | body | yes | Number of activation records to skip for pagination |
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
{
"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
{
"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)
/license/activation-deleteSend the selected activation ID with the short-lived session token. Refresh the activation list after a successful deletion.
Request Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
activation_id | integer | body | yes | ID of the activation selected by the customer |
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
{
"success": {
"deleted": true
}
}Deletion rules:
- Only display delete buttons when
activations_deletableistrue. - 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.
- Open the CML Marketplace.
- Complete a test order using an email address you can access.
- Wait for the temporary license email.
- Open the Activations demo.
- Enter the email and, when required by the selected mode, the issued license.
- Complete Turnstile, request the email code, and exchange it for an activation session.
- 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 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
- Download the SDK
- Extract Files
The SDK package contains two files:
CheckMyLicense.mqh– The reusable SDK librarySHA26.mqh– The reusable component for SHA-256 hashingCheckMyLicense.mq5– Example implementation script
- Copy to MetaTrader Directory
Copy
CheckMyLicense.mqhandSHA26.mqhto your MetaTrader 5 Include folder:C:\Users\YourUsername\AppData\Roaming\MetaQuotes\Terminal\[TerminalID]\MQL5\Include\ - 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's email
input string InpLicence = ""; // License key2. 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 Code | Description | Solution |
|---|---|---|
EX1001 | Email not provided | Enter your email address in the input parameters (InpEmail) |
EX1002 | License key not provided | Enter your license key in the input parameters (InpLicence) |
EX1003 | Program name not configured | Set #define ProgramName to match your program name in CheckMyLicense |
EX1004 | License validation failed at startup | Verify your license key format and API credentials |
EX2001 | Payload encoding failed | Verify your #define APISecret is correctly set from Organization settings |
EX2002 | HMAC-SHA256 signature computation failed | Verify #define APIID and #define APISecret are correct |
EX3001 | WebRequest URL not allowed in MT5 | Add https://vwqretlvkrravzguxydw.functions.eu-west-2.nhost.run/v1 to Tools → Options → Expert Advisors → Allow WebRequest for URLs |
EX3003 | Connection error or timeout | Check your internet connection; if persists, verify the server URL is accessible |
EX4001-EX4005 | Cryptographic operation failed (Base64 encoding/decoding) | Verify the license key is valid; check that SHA256.mqh is included in your project |
400 Bad Request | Invalid payload or malformed request | Verify email format is correct and license key contains valid 32 hex characters |
401 Unauthorized | License invalid, expired, or not owned by email | Verify the email matches the license owner; check license hasn't expired in your dashboard |
404 Not Found | License or program not found | Verify the license key exists and the program name matches your CheckMyLicense dashboard |
500 Server Error | CheckMyLicense server error | Try again later; if error persists, contact support |
Testing Your Implementation
- Compile – Compile your MQL5 file in MetaEditor (F7)
- Attach to Chart – Drag your EA/Indicator to a chart
- Enter Credentials – Input your email and license key in the parameters dialog
- Check Logs – Open the Experts tab to see verification results
- 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
requestslibrary for HTTP callscryptographylibrary for token validation- Active CheckMyLicense organization account
- Your organization's API ID and API Secret (from Settings → Organization)
Installation Steps
- Install Dependencies
pip install requests cryptography - Download the SDK
Download the Python SDK v4 from your organization page (Available SDK section).
- Extract the File
The SDK package contains a single file:
CheckMyLicenseOnline.py– The CLI tool and library
- 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-configExample 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-configOffline 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
| Parameter | Required | Description |
|---|---|---|
--api-id | Online only | Organization API ID from Settings → Organization |
--api-secret | Online only | Organization API Secret from Settings → Organization |
--endpoint | Online only | CheckMyLicense API endpoint URL |
--license | Yes | License key |
--email | Online only | License owner's email address |
--program | Yes | Program name (must match CheckMyLicense) |
--v | No | Protocol version: 1, 2, 3, or 4 (default: 4) |
--offline | No | Use cached offline token if available |
--show-config | No | Display program_configuration from response |
--target-id | No | Target machine/device identifier (optional) |
Protocol Versions
| Version | Features |
|---|---|
v4 (default) | Includes program_configuration, offline_token with config in signature, and license restriction enforcement |
v3 | Includes program_configuration, offline_token with config in signature |
v2 | Basic response with validity, limit, used (no configuration) |
v1 | Legacy 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
fetchor falls back tohttpsmodule 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
- Download the SDK
Download the Node.js SDK from your organization page (Available SDK section).
- Extract the File
The SDK package contains a single file:
CheckMyLicenseOnline.js– The SDK module
- Place in Your Project
Copy
CheckMyLicenseOnline.jsinto your project directory, then require it directly — nonpm installneeded. - 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
| Parameter | Required | Description |
|---|---|---|
apiId | Online only | Organization API ID from Settings → Organization |
apiSecret | Online only | Organization API Secret from Settings → Organization |
endpoint | Online only | CheckMyLicense API endpoint URL |
license | Yes | License key to verify |
email | Online only | License owner's email address |
program | Yes | Program name (must match CheckMyLicense dashboard) |
targetId | No | Target machine/device identifier |
targetHost | No | Target host identifier |
type | No | Account type identifier |
version | No | Protocol version: '1', '2', '3', or '4' (default: '4') |
offline | No | Set true to use cached offline token if available |
verifyServerSig | No | Set true to validate the server's response signature |
Return Value
| Property | Type | Description |
|---|---|---|
success | boolean | Whether verification succeeded |
message | string | Human-readable result or error description |
source | 'online' | 'offline' | Whether the result came from the server or cached token |
data | object | Full parsed server response data |
programConfiguration | object | Program-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.