> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nmcrate.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Purchase verification

> Short-lived XXX-XXX-XXX codes that prove product ownership to third parties, without exposing any user data.

Purchase verification lets a buyer prove they own a product to anyone, a Discord bot, a support desk, a seller's own site, using a disposable code instead of their account details.

<Info>
  **Tier:** Free, no studio key requried.
</Info>

## How the flow works

1. A logged-in buyer generates a code for a product they own. It looks like `HFP-QCN-VJ7`.
2. The buyer pastes the code wherever verification is needed (e.g. a `/verify` Discord command).
3. Your integration calls the **public check endpoint** with the code.
4. You get back a verdict plus the product it proves ownership of, and nothing about the buyer.

Codes are:

* **Short-lived**, valid for 10 minutes.
* **Single-use**, the first successful check consumes the code; anyone replaying it gets `already_used`. Only one checker can ever receive `valid: true`.
* **Privacy-preserving**, the check response never contains the buyer's identity, so no user data changes hands.

## Generating a code (buyer side)

The easiest way: send buyers to the verification page on the store,

```text theme={null}
https://nmcrate.com/purchase/verify/{productId}
```

It shows the product, the code in large type with a copy button, and a live expiry countdown. Programmatic alternative: `POST /api/purchases/verify/{productId}` with a logged-in NMCrate session. `productId` accepts the product slug or id.

```json theme={null}
{
  "token": "HFP-QCN-VJ7",
  "expiresAt": "2026-07-13T19:44:12.107Z",
  "product": {
    "id": "craftengine_expansion",
    "slug": "craftengine-support",
    "name": "CraftEngine Support"
  }
}
```

Requesting again while a code is still fresh returns the **same** code rather than minting a new one. If the account doesn't own the product, the response is `403` with `code: "NOT_OWNED"`.

## Checking a code (integrator side)

`GET /api/purchases/verify/check/{token}`, **public, no authentication**; the code itself is the proof.

```bash theme={null}
curl https://nmcrate.com/api/purchases/verify/check/HFP-QCN-VJ7
```

```json theme={null}
{
  "valid": true,
  "product": {
    "id": "craftengine_expansion",
    "slug": "craftengine-support",
    "name": "CraftEngine Support"
  },
  "generatedAt": "2026-07-13T19:34:12.134Z"
}
```

Input is forgiving: lowercase, missing dashes, and stray whitespace are normalized (`hfpqcnvj7` works). The response is always HTTP `200`, the verdict is in the body:

| `valid` | `reason`         | Meaning                                                                                                      |
| ------- | ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `true`  | ,                | The code proves ownership of `product`. It is now consumed.                                                  |
| `false` | `invalid_format` | The input can't be a code (must be 9 characters, `A–Z` / `2–9`).                                             |
| `false` | `not_found`      | No such code, mistyped, or expired long enough ago to be purged.                                             |
| `false` | `expired`        | The code existed but its 10 minutes are up.                                                                  |
| `false` | `already_used`   | The code was already checked once. Treat with suspicion, someone may be replaying a code they saw elsewhere. |

<Note>
  Always compare the returned `product.id` (or `slug`) against the product **you** are verifying for. A valid code proves ownership of the product it was generated for, not of your product specifically.
</Note>

## Example: Discord bot verification

```text theme={null}
User:  /verify HFP-QCN-VJ7
Bot →  GET /api/purchases/verify/check/HFP-QCN-VJ7
Bot:   checks valid === true && product.slug === "craftengine-support"
Bot:   assigns the "Customer" role
```

Because codes are single-use and expire in 10 minutes, a code screenshotted or pasted in a public channel can't be reused by someone else after your bot has checked it.
