> ## 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, 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 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'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:

```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 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:

```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 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_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.
* **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

```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 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:

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