---
title: "License Verification API"
description: "Integrate 3DIMLI's games license verification API into your launcher or license server. Verify buyer purchases using order item IDs as license keys."
canonical_url: "https://support.3dimli.com/creating-products/games/license-verification-api"
md_url: "https://support.3dimli.com/creating-products/games/license-verification-api.md"
source: "docs/creating-products/games/license-verification-api.md"
x_aeo_version: "1.0"
estimated_tokens: 5312
---

# Games License Verification API

When you sell a game on 3DIMLI, each buyer receives a unique **Order Item ID** after purchase. This acts as their **license key**. Your game launcher or license server can call 3DIMLI's verification API to confirm whether a key is legitimate, which license tier was purchased, and whether the game is still published.

:::info Who is this for?
This page is for **developers listing games on 3DIMLI** who want to add license activation and verification to their games or launchers. For a step-by-step integration walkthrough, see the [Games License Integration Guide](/games-license-integration).
:::

## How It Works

| Step | What happens | Who |
|:---:|---|---|
| **1** | Buyer purchases your game on 3DIMLI | Buyer |
| **2** | Buyer receives an **Order Item ID** (license key) via email and in their dashboard | 3DIMLI |
| **3** | Buyer enters the license key in your launcher | Buyer |
| **4** | Your launcher sends the key to your license server, which forwards it to 3DIMLI authenticated with your seller token | Your launcher + license server |
| **5** | 3DIMLI responds: `valid: true/false` + `productName` + `licenseName` + `productStatus` | 3DIMLI |
| **6** | Your launcher unlocks the game based on the license tier, optionally enforces device limits | Your launcher |

**3DIMLI answers one question:** *"Was this key legitimately purchased?"*

**You handle everything else:** activation count, device binding, concurrent sessions, and feature gating based on `licenseName` on your own server.

---

## API Reference

### Endpoint

```http title="POST"
https://www.3dimli.com/api/games/v1/verify
```

This endpoint requires a **seller license-verification API token** in the `Authorization` header. Generate one from [Dashboard → Settings → API Tokens](https://www.3dimli.com/dashboard/seller/settings/api-tokens). The token is shown once at creation time — copy and store it securely.

---

### Request Headers

| Header | Required | Description |
|---|---|---|
| `Authorization` | Yes | `Bearer <your_token>` — your seller license-verification token |
| `Content-Type` | Yes | `application/json` |

---

### Request Body

```json title="Request"
{
 "key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "product_slug": "/my-game"
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | `string` (UUID) | Yes | The buyer's Order Item ID (their license key) |
| `product_slug` | `string` | Yes | Your game's URL slug from the store (e.g., `/my-game`) |

The `key` must be a valid UUID (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`). The `product_slug` must be a non-empty string.

:::note Slug format
The API normalizes the slug automatically. All of these are accepted: `/my-game`, `my-game`, `games/my-game`.
:::

---

### Responses

### Valid License

```json title="200 OK"
{
 "valid": true,
 "productName": "My Game",
 "licenseName": "Standard EULA",
 "productStatus": "PUBLISHED",
 "variant": {
 "title": "Deluxe"
 }
}
```

### Invalid License

```json title="200 OK"
{
 "valid": false
}
```

### Unauthorized

```json title="401 Unauthorized"
{
 "error": "Unauthorized"
}
```

### Forbidden

```json title="403 Forbidden"
{
 "error": "Forbidden"
}
```

### Rate Limited

```http title="429 Too Many Requests"
Retry-After: 45
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 45
```

#### Response Fields

| Field | Type | Description |
|---|---|---|
| `valid` | `boolean` | `true` if the key is a legitimate, active, non-refunded purchase of this game |
| `productName` | `string \| null` | The game title on 3DIMLI (e.g., "My Game") |
| `licenseName` | `string \| null` | The license tier the buyer purchased (e.g., "Standard License", "Demo / Trial", "Open Source") |
| `productStatus` | `string` | The game's lifecycle status on 3DIMLI (e.g., `PUBLISHED`, `UNPUBLISHED`). Useful for launchers that gate updates or new sessions when the game is unpublished |
| `variant` | `object` | Included only when the purchased game has variants enabled. Contains `title` (e.g., the platform or edition the buyer selected) |

#### When `valid` Returns `false`

A license is invalid if **any** of the following are true:

- The Order Item ID does not exist
- The order status is not `COMPLETED`
- The order has been refunded
- The `product_slug` does not match the purchased game
- The product is not a **Games** type

:::note Security
The API never leaks information about why a key is invalid. It always returns `` with no additional details.
:::

---

### Validation Sequence

The API processes each request in this order:

| Step | Check | Failure response |
|:---:|---|---|
| 1 | `Authorization: Bearer
` header present and non-empty | 401 |
| 2 | `key` is a valid UUID and `product_slug` is non-empty | 400 `` |
| 3 | Token is valid and not revoked | 401 |
| 4 | Token owner has an active platform plan | 403 with subscription error |
| 5 | Token owner is the seller of the product at `games/
` | 403 |
| 6 | Client IP present | 400 `` |
| 7 | Rate limit: 30 req / 60s / IP | 429 with `Retry-After` |
| 8 | OrderItem exists with matching key AND game slug | `` |
| 9 | Order item is in a verifiable state and not refunded | `` |
| 10 | Product type is `Games` | `` |

Steps 8–10 always return identical `` — no information is leaked about why it failed.

---

### Status Codes

| Code | Meaning |
|---|---|
| `200` | Request processed. Check the `valid` field for the result |
| `400` | Missing or invalid fields (`key` must be a valid UUID, `product_slug` must be non-empty) |
| `401` | Missing, malformed, invalid, or revoked token |
| `403` | Token is valid but you don't own this game, or your platform subscription has lapsed |
| `429` | Rate limit exceeded. Check `Retry-After` header |
| `500` | Server error. Retry later |
| `503` | Rate limiter unavailable. Retry later |

---

### Rate Limiting

| | |
|---|---|
| **Limit** | 30 requests per 60 seconds |
| **Scope** | Per IP address (applied after token authentication) |
| **Headers** | `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` |

:::warning Centralized license servers
If all your players' requests go through a single license server, those requests share one IP rate limit. **Cache verification results** on your server to stay within limits.
:::

---

## Generating Your Seller Token

1. Sign in to your seller account on [3DIMLI](https://www.3dimli.com).
2. Open [Dashboard → Settings → API Tokens](https://www.3dimli.com/dashboard/seller/settings/api-tokens).
3. Generate a **license-verification** token.
4. Copy the raw token shown in the one-time dialog and store it in a secret manager (for example, your launcher's backend environment).

:::warning Keep the token server-side
Treat the token like a password. Do **not** ship it inside the player-facing game binary, where it can be extracted. Call the verify endpoint from your own license server (see examples below) and embed only the URL of your server in the game.
:::

The token is bound to your seller account and can only verify keys for games you own. Verification requests for any other seller's game will return `403 Forbidden`.

---

## Finding Your Product Slug

Your product slug is the URL path of your game in the store. Find it in the browser address bar when viewing your product page:

```
https://www.3dimli.com/games/<your-game-slug>
 └── product_slug ┘
```

For example, if your game page is at `https://www.3dimli.com/games/my-game`, your product slug is `/my-game` (the API also accepts `my-game` or `games/my-game`).

:::note
The product slug is **not a secret** — it's visible in the store URL. It acts as a scope guard so a key purchased for Game A cannot be used to activate Game B.
:::

---

## Integration Examples

### Quick Test

```bash title="cURL"
curl -X POST https://www.3dimli.com/api/games/v1/verify \
 -H "Authorization: Bearer <your_token>" \
 -H "Content-Type: application/json" \
 -d '{
 "key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "product_slug": "/my-game"
 }'
```

---

### License Server with Activation Limits

The recommended pattern: your launcher talks to your license server, and your license server holds the seller token and calls 3DIMLI. This keeps the token off player machines.

### Node.js

```javascript title="server.js" showLineNumbers
const express = require("express");
const app = express();
app.use(express.json());

// ── Config ──────────────────────────────────────────────
const PRODUCT_SLUG = "/my-game";
const SELLER_TOKEN = process.env.DIMLI_SELLER_TOKEN; // never hardcode

// Map your license tier names to activation limits
const ACTIVATION_LIMITS = {
 "Standard License": 3, // up to 3 devices
 "Demo / Trial License": 1, // single demo device
 "Open Source License": -1, // unlimited
};

// ── Store (use a database in production) ────────────────
const activations = new Map(); // key -> { licenseName, devices: Set }

// ── Activate ────────────────────────────────────────────
app.post("/activate", async (req, res) => {
 const { licenseKey, deviceId } = req.body;

 if (!licenseKey || !deviceId) {
 return res.status(400).json({ error: "licenseKey and deviceId required" });
 }

 // highlight-start
 // Verify with 3DIMLI
 const response = await fetch(
 "https://www.3dimli.com/api/games/v1/verify",
 {
 method: "POST",
 headers: {
 "Authorization": `Bearer ${SELLER_TOKEN}`,
 "Content-Type": "application/json",
 },
 body: JSON.stringify({ key: licenseKey, product_slug: PRODUCT_SLUG }),
 }
 );
 const result = await response.json();
 // highlight-end

 if (response.status === 401 || response.status === 403) {
 // Misconfiguration on your side - alert ops, don't blame the player
 console.error("3DIMLI auth failed:", result);
 return res.status(503).json({ error: "License service unavailable" });
 }

 if (!result.valid) {
 return res.status(403).json({ error: "Invalid license key" });
 }

 // Optional: refuse new activations when the game is unpublished
 if (result.productStatus === "UNPUBLISHED") {
 // Existing players can still play offline; up to you whether to gate new devices
 }

 // Get or create activation record
 let record = activations.get(licenseKey);
 if (!record) {
 record = { licenseName: result.licenseName, devices: new Set() };
 activations.set(licenseKey, record);
 }

 // Already activated on this device
 if (record.devices.has(deviceId)) {
 return res.json({ activated: true, licenseName: record.licenseName });
 }

 // Enforce activation limit
 const limit = ACTIVATION_LIMITS[record.licenseName] ?? 1;
 if (limit !== -1 && record.devices.size >= limit) {
 return res.status(403).json({
 error: `Activation limit reached (${limit}). Deactivate a device first.`,
 });
 }

 record.devices.add(deviceId);
 res.json({ activated: true, licenseName: record.licenseName });
});

// ── Deactivate ──────────────────────────────────────────
app.post("/deactivate", (req, res) => {
 const { licenseKey, deviceId } = req.body;
 const record = activations.get(licenseKey);
 if (record) record.devices.delete(deviceId);
 res.json({ deactivated: true });
});

app.listen(3000, () => console.log("Game license server on :3000"));
```

### Python

```python title="server.py" showLineNumbers
import os
from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

# ── Config ──────────────────────────────────────────────
PRODUCT_SLUG = "/my-game"
SELLER_TOKEN = os.environ["DIMLI_SELLER_TOKEN"] # never hardcode

ACTIVATION_LIMITS = {
 "Standard License": 3,
 "Demo / Trial License": 1,
 "Open Source License": -1, # unlimited
}

# ── Store (use a database in production) ────────────────
activations = {} # key -> { "licenseName": str, "devices": set }

# ── Activate ────────────────────────────────────────────
@app.route("/activate", methods=["POST"])
def activate():
 data = request.json
 license_key = data.get("licenseKey")
 device_id = data.get("deviceId")

 if not license_key or not device_id:
 return jsonify({"error": "licenseKey and deviceId required"}), 400

 record = activations.get(license_key)

 if not record:
 # highlight-start
 # Verify with 3DIMLI
 resp = requests.post(
 "https://www.3dimli.com/api/games/v1/verify",
 headers={"Authorization": f"Bearer {SELLER_TOKEN}"},
 json={"key": license_key, "product_slug": PRODUCT_SLUG},
 )
 result = resp.json()
 # highlight-end

 if resp.status_code in (401, 403):
 return jsonify({"error": "License service unavailable"}), 503

 if not result.get("valid"):
 return jsonify({"error": "Invalid license key"}), 403

 record = {"licenseName": result.get("licenseName"), "devices": set()}
 activations[license_key] = record

 if device_id in record["devices"]:
 return jsonify({"activated": True, "licenseName": record["licenseName"]})

 limit = ACTIVATION_LIMITS.get(record["licenseName"], 1)
 if limit != -1 and len(record["devices"]) >= limit:
 return jsonify({"error": f"Activation limit reached ({limit})"}), 403

 record["devices"].add(device_id)
 return jsonify({"activated": True, "licenseName": record["licenseName"]})

# ── Deactivate ──────────────────────────────────────────
@app.route("/deactivate", methods=["POST"])
def deactivate():
 data = request.json
 record = activations.get(data.get("licenseKey"))
 if record:
 record["devices"].discard(data.get("deviceId"))
 return jsonify({"deactivated": True})

if __name__ == "__main__":
 app.run(port=3000)
```

---

### Client-Side Activation (In Your Game / Launcher)

How the player's launcher calls your license server on startup. The seller token is **not** present here — only the URL of your license server.

### JavaScript / Electron

```javascript title="license-client.js"
const os = require("os");
const crypto = require("crypto");

function getDeviceId() {
 const raw = `${os.hostname()}-${os.platform()}-${os.cpus()[0]?.model}`;
 return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32);
}

async function activate(licenseKey) {
 const response = await fetch("https://your-license-server.com/activate", {
 method: "POST",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ licenseKey, deviceId: getDeviceId() }),
 });

 const result = await response.json();

 if (result.activated) {
 // Save key locally so player doesn't re-enter it
 saveToStorage("licenseKey", licenseKey);
 unlockGame(result.licenseName);
 return true;
 }

 showError(result.error);
 return false;
}
```

### Python

```python title="license_client.py"
import hashlib
import platform
import requests

def get_device_id():
 raw = f"{platform.node()}-{platform.system()}-{platform.processor()}"
 return hashlib.sha256(raw.encode()).hexdigest()[:32]

def activate(license_key):
 response = requests.post(
 "https://your-license-server.com/activate",
 json={"licenseKey": license_key, "deviceId": get_device_id()},
 )
 result = response.json()

 if result.get("activated"):
 save_to_storage("licenseKey", license_key)
 unlock_game(result["licenseName"])
 return True

 print(f"Activation failed: {result.get('error')}")
 return False
```

---

## Architecture

```
 ┌─────────────────────────────────┐
 │ YOUR LICENSE SERVER │
 │ (holds seller Bearer token) │
 ┌───────────────┐ POST │ │ POST ┌──────────────┐
 │ │ /activate│ 1. Receive key + deviceId │ + Bearer │ │
 │ GAME LAUNCHER│─────────▶│ 2. Call 3DIMLI verify API ───────────▶│ 3DIMLI │
 │ │ │ 3. Cache result │ │ API │
 │ License key │◀─────────│ 4. Check activation limits │◀─────────│ │
 │ + device ID │ Result │ 5. Register device │ valid │ /games/v1/ │
 └───────────────┘ │ 6. Return activated/denied │ + status │ verify │
 │ │ └──────────────┘
 │ YOUR RESPONSIBILITY: │
 │ • Activation limits │
 │ • Device tracking │
 │ • Concurrent session checks │
 │ • Feature gating │
 │ • Offline grace periods │
 │ • Token storage (server-side) │
 └─────────────────────────────────┘
```

---

## Best Practices

### Caching

Don't call the 3DIMLI API on every launcher start. Cache the result on your server and re-verify periodically.

```javascript title="Caching example"
const PRODUCT_SLUG = "/my-game";
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours

async function verifyWithCache(licenseKey) {
 const cached = activations.get(licenseKey);

 // Use cache if fresh
 if (cached && Date.now() - cached.verifiedAt < CACHE_TTL) {
 return cached;
 }

 // Re-verify with 3DIMLI
 const res = await fetch("https://www.3dimli.com/api/games/v1/verify", {
 method: "POST",
 headers: {
 "Authorization": `Bearer ${process.env.DIMLI_SELLER_TOKEN}`,
 "Content-Type": "application/json",
 },
 body: JSON.stringify({ key: licenseKey, product_slug: PRODUCT_SLUG }),
 });
 const result = await res.json();

 if (!result.valid) {
 activations.delete(licenseKey); // Key revoked (e.g., refund)
 return null;
 }

 // Update cache timestamp
 if (cached) cached.verifiedAt = Date.now();
 return cached;
}
```

### Offline & Refunds

- **Offline grace period.** If your server or 3DIMLI is unreachable, let the player continue for a set time (e.g., 7 days) before requiring re-verification. Single-player games should lean toward generous offline windows.
- **Refund handling.** When a buyer gets a refund, 3DIMLI returns `valid: false`. On your next re-verify cycle, revoke access gracefully. Don't delete saved games or progress.

### Device IDs

Generate stable, unique device identifiers using hardware fingerprints (hostname + OS + CPU). Store the ID locally so it persists across sessions. Avoid identifiers that change after driver updates.

### Handling `productStatus: UNPUBLISHED`

If you unpublish your game on 3DIMLI, existing buyers still hold valid licenses. The API will keep returning `valid: true` for them but `productStatus: "UNPUBLISHED"`. Decide your launcher policy: most games let existing players keep playing and only block **new** activations.

### Rate Limits

With 30 requests/minute per IP, a centralized server handling many players should cache aggressively. Batch or schedule re-verifications rather than verifying on every launch.

### Token Hygiene

- Store the seller token in a secret manager or environment variable on your license server. Never commit it to source control.
- Rotate the token from your [API Tokens](https://www.3dimli.com/dashboard/seller/settings/api-tokens) page if you suspect leakage.
- A single token can verify any game you own — there's no need to mint a different token per title.

---

## FAQ

### Where does the buyer find their license key?

The Order Item ID is available in:
1. The **purchase confirmation email** from 3DIMLI
2. **Dashboard → Orders** → expand order → **License Product ID**
3. **Dashboard → Downloads** → license details → **Item ID**

All locations include a copy button for convenience.

### What happens if a buyer gets a refund?

The API returns `` for refunded orders. Your license server should handle this on the next re-verification by revoking access gracefully without deleting save data or progress.

### Do I need an API token?

Yes. The games verify endpoint requires a seller license-verification token in the `Authorization` header. Generate one from [Dashboard → Settings → API Tokens](https://www.3dimli.com/dashboard/seller/settings/api-tokens).

### Can I put the token directly inside my game binary?

Strongly discouraged. Anyone who downloads your game can extract strings from the binary and use the token to verify keys for any of your titles. Always proxy verification through a license server you control and keep the token there.

### Can one key work across multiple games?

No. Each key (Order Item ID) is tied to a specific game. The `product_slug` in the request must match the game the buyer purchased.

### What does licenseName return?

The exact name of the license tier the buyer selected during purchase, for example "Standard License", "Demo / Trial License", "Open Source License", or any custom name you defined when creating your game.

### What does productName return?

The title of your game on 3DIMLI, for example "My Game". This is included in valid responses so your launcher can display which game was activated.

### What does productStatus return?

The lifecycle status of the game on 3DIMLI — typically `PUBLISHED` or `UNPUBLISHED`. Existing buyers retain valid licenses even after a game is unpublished; your launcher decides whether to gate new activations or new sessions in that case.

### What is variant?

If your game has variants enabled (e.g., Standard / Deluxe / Platform-specific editions), the response includes the variant the buyer purchased. The field is omitted when the game has no variants.

### What is product_slug and where do I find it?

The product slug is the URL path of your game in the 3DIMLI store. For example, if your game page is at `https://www.3dimli.com/games/my-game`, the slug is `/my-game`. It's not a secret — it prevents keys bought for one game from activating a different game.

---

## Related

- [Games Product Type](/creating-products/games). Creating and listing your game on 3DIMLI
- [Games License Integration Guide](/games-license-integration). Full step-by-step developer walkthrough
- [Software License Verification API](/creating-products/software/license-verification-api). Equivalent endpoint for software products
