Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt

Use this file to discover all available pages before exploring further.

You ship a desktop application (Windows, macOS, Linux — Electron, .NET, Qt, native, whatever) and you want it to activate against Shoppex license keys. Each customer’s purchase issues a key; your app calls Shoppex to validate it and binds to the user’s hardware so the key can’t be shared. This tutorial walks through the activation flow end to end.

How Shoppex licenses work

When a buyer purchases a product fulfilled by Licenses, Shoppex issues a license object with:
  • A unique license_key string the buyer sees and enters into your app.
  • Status: ACTIVE, SUSPENDED, REVOKED, or EXPIRED.
  • Optional HWID binding — when the first activation comes in with a hardware ID, that HWID gets bound. Future activations from different hardware fail.
  • Optional IP allowlistallowed_ips array; non-matching IPs are rejected.
  • Optional max_uses and expires_at — limits on usage count and validity period.
Your job is to call the validate endpoint, hand it the buyer’s key plus their HWID, and act on the response.

What you’ll need

  • A Shoppex API key with the licenses.read scope. Create one at Settings → Developer → API Keys.
  • A way to compute a stable hardware fingerprint in your app. Common approaches: motherboard serial + CPU ID on Windows (wmic), IOPlatformUUID on macOS, /etc/machine-id on Linux, or any cross-platform library that derives a stable hash from hardware identifiers.

The endpoint

POST https://api.shoppex.io/dev/v1/licenses/validate
Authorization: Bearer shx_your_key
Content-Type: application/json
Request body:
{
  "key": "ABCD-1234-EFGH-5678",
  "product_id": "prod_xxxxxxxxxxxx",
  "hardware_id": "stable-hwid-string",
  "ip": "203.0.113.45"
}
  • key and product_id are required.
  • hardware_id is optional but recommended if you want anti-sharing.
  • ip is optional. If you don’t pass it, Shoppex reads x-forwarded-for / x-real-ip from the request headers and uses that.

Response

On success, you get the license object back wrapped in Shoppex’s standard envelope:
{
  "data": {
    "uniqid": "lic_xxxxxxxxxxxx",
    "license_key": "ABCD-1234-EFGH-5678",
    "product_id": "prod_xxxxxxxxxxxx",
    "hardware_id": "stable-hwid-string",
    "hwid_pending": false,
    "status": "ACTIVE",
    "uses": 5,
    "max_uses": null,
    "allowed_ips": [],
    "expires_at": null,
    "customer_email": "[email protected]"
  }
}
On failure, you get an error response with one of these codes:
  • LICENSE_NOT_FOUND — key doesn’t exist in this shop.
  • LICENSE_SUSPENDED — license is suspended (merchant-side action).
  • LICENSE_REVOKED — license was revoked.
  • LICENSE_EXPIRED — past expires_at.
  • LICENSE_HWID_MISMATCH — the HWID you passed doesn’t match the bound one.
  • LICENSE_IP_BLOCKED — the resolved IP isn’t in allowed_ips.
  • LICENSE_MAX_USES_REACHEDuses >= max_uses.

HWID binding behavior

The HWID logic is automatic:
  • First call with a HWID, license hwid_pending: true — Shoppex binds the HWID to the license and returns success. The license is now locked to that machine.
  • Subsequent calls with the same HWID — pass through. License returned.
  • Call with a different HWID — Shoppex returns LICENSE_HWID_MISMATCH. The buyer is trying to activate on a second machine.
When the buyer legitimately needs a new machine (replacement laptop, reinstalled OS), you or the merchant can call:
POST /dev/v1/licenses/{license_id}/reset-hwid
This unbinds the HWID. The next validate call with a new HWID will bind that one instead. For workflows where buyers self-service the HWID reset (e.g. through your app’s “Move to a new machine” button), there’s also a key-scoped variant:
PATCH /dev/v1/licenses/keys/{key}/hwid
Body: { "product_id": "prod_xxx", "hardware_id": null }
Setting hardware_id to null puts the license back in hwid_pending: true state. Setting it to a string binds that HWID directly.

Rate limits

The Dev API uses a token bucket — by default 30 tokens, refilling 10 tokens every 2 seconds. Validate calls count against this. For most apps that’s plenty:
  • An app that validates once on launch hits the limit only if a single customer is brute-forcing.
  • Apps that validate periodically (e.g. every hour for “online required”) should batch and back off on 429.
Response headers tell you where you stand: x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset. On 429, respect the retry-after header.

A minimal activation flow

async function activate(key: string) {
  const hwid = getStableHwid();  // your hardware-fingerprinting code

  const response = await fetch('https://api.shoppex.io/dev/v1/licenses/validate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SHOPPEX_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      key,
      product_id: 'prod_your_product_id',
      hardware_id: hwid,
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    switch (error.error.code) {
      case 'LICENSE_HWID_MISMATCH':
        return { ok: false, reason: 'This license is already activated on another machine.' };
      case 'LICENSE_EXPIRED':
        return { ok: false, reason: 'Your license has expired. Please renew.' };
      case 'LICENSE_SUSPENDED':
      case 'LICENSE_REVOKED':
        return { ok: false, reason: 'This license is no longer valid.' };
      case 'LICENSE_NOT_FOUND':
        return { ok: false, reason: 'Invalid license key.' };
      default:
        return { ok: false, reason: 'Activation failed. Try again later.' };
    }
  }

  const { data: license } = await response.json();
  return { ok: true, license };
}

Caching: how to handle “the user is offline”

Shoppex doesn’t currently issue signed offline-validation tokens. Every validate call hits the API live. That means a totally-offline machine can’t validate. Most apps handle this by caching successful validations for a grace period: store the last successful validation timestamp locally (signed by your app’s own key to prevent tampering), and let the app run for N hours / N days before requiring a fresh online call. Pick the grace window based on what’s acceptable in your domain — game keys often run with 24-48 hour grace, business software often runs with 30 days.

SDK note

The official @shoppexio/sdk for Node/TypeScript doesn’t include a dedicated licenses service class yet. You can still call the endpoint via the SDK’s typed raw client:
import { Shoppex } from '@shoppexio/sdk';
const client = new Shoppex({ apiKey: process.env.SHOPPEX_API_KEY });
const response = await client.raw.POST('/dev/v1/licenses/validate', {
  body: { key, product_id, hardware_id },
});
Or just use plain fetch — both work. There’s no C#, .NET, Python, or other language-specific desktop SDK in the repo yet.

Common pitfalls

  • HWID that isn’t stable. Some hardware-id schemes change on OS update or BIOS reset. Test by validating, rebooting, validating again — same HWID? If not, you’ll burn legitimate buyers.
  • Validating on every API call inside the app. Rate limits will hurt you. Validate on launch + every N hours; cache between.
  • Not handling 429. Even a well-behaved app gets rate-limited if a user manually retries 10x. Honor retry-after.
  • No fallback for transient errors. Network blip ≠ invalid license. On 500/503/ connection errors, fall back to your cached grace window, don’t lock the user out.