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

# Validate licenses from your own desktop app

> Wire your installer's activation flow to Shoppex licenses with HWID and IP locking.

You ship a desktop application — Windows, macOS, or Linux, using Electron, .NET, Qt, native,
or another stack. You want it to activate against Shoppex license keys. Each customer's
purchase issues a key. Your app calls Shoppex to validate the key and binds it to the user's
hardware so the key cannot 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 allowlist** — `allowed_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 will 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 plus CPU ID on Windows (`wmic`), `IOPlatformUUID` on macOS, or `/etc/machine-id` on
  Linux. Any cross-platform library that derives a stable hash from hardware identifiers also
  works.

## The endpoint

```
POST https://api.shoppex.io/dev/v1/licenses/validate
Authorization: Bearer shx_your_key
Content-Type: application/json
```

Request body:

```json theme={"system"}
{
  "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 do not 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:

```json theme={"system"}
{
  "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": "buyer@example.com"
  }
}
```

On failure, you get an error response with one of these codes:

* `LICENSE_NOT_FOUND` — key does not 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 does not match the bound one.
* `LICENSE_IP_BLOCKED` — the resolved IP is not in `allowed_ips`.
* `LICENSE_MAX_USES_REACHED` — `uses` >= `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.
* **Later 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, for example through your app's
"Move to a new machine" button, there is 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 is plenty:

* An app that validates once on launch hits the limit only if a single customer is
  brute-forcing.
* Apps that validate periodically, for example every hour for an "online required" check,
  must 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

```ts theme={"system"}
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 does not currently issue signed offline-validation tokens. Every validate call hits
the API live. That means a totally-offline machine cannot 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. Let the app run for N hours or N days before requiring a fresh online call.

Pick the grace window based on what is 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 does not include a dedicated `licenses`
service class yet. You can still call the endpoint through the SDK's typed raw client:

```ts theme={"system"}
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 use plain `fetch` — both work. There is no C#, .NET, Python, or other language-specific
desktop SDK in the repo yet.

## Common pitfalls

* **HWID that is not stable.** Some hardware-id schemes change on OS update or BIOS reset.
  Test by validating, rebooting, validating again — same HWID? If not, you will burn
  legitimate buyers.
* **Validating on every API call inside the app.** Rate limits will hurt you. Validate on
  launch and every N hours. Cache between validations.
* **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.** A network blip is not the same as an invalid license.
  On `500`/`503`/connection errors, fall back to your cached grace window. Do not lock the
  user out.
