# Cartcord license verification API

Enable this optional API in a saved key product's Delivery settings. Generate random,
unguessable keys of at least 16 characters and upload them through the existing key
inventory. A key must have been sold and assigned to an eligible completed order.
Unused inventory cannot activate. Re-sending an order does not allocate another key.

`POST https://cartcord.com/api/v1/licenses/verify`

```json
{
  "product_id": "YOUR_PRODUCT_UUID",
  "license_key": "THE_BUYERS_RANDOM_LICENSE_KEY",
  "hwid": "AN_APPLICATION_SCOPED_RANDOM_DEVICE_IDENTIFIER",
  "nonce": "A_FRESH_RANDOM_NONCE_AT_LEAST_16_CHARS"
}
```

Use an installation identifier generated for your application rather than collecting
hardware serial numbers. Inform buyers about device binding. The server stores a
per-license digest, not the raw identifier. Changing the identifier consumes another
activation unless the seller resets the key's devices. A repeated request for the
same key and device does not consume an additional activation.

The response contains `payload` (an exact JSON string), `signature` (base64url),
`algorithm: "Ed25519"`, and `kid`. The signed payload binds the result to the product,
key fingerprint, device fingerprint, request nonce, issuer, issue time and expiry.
It expires after five minutes. `valid: false` is a signed business result; malformed
requests return HTTP 400, rate limits return 429, and infrastructure failures return
5xx. Never treat an unsigned error or an unreachable server as a valid license.

Fetch the public verification key at
`GET https://cartcord.com/api/v1/licenses/public-key` over trusted HTTPS during your
integration setup. **Pin that public key and kid in your application or trusted
server configuration.** Do not trust a key supplied with an untrusted verification
response. Keep an explicit key rotation/update procedure for your application.

Example using Web Crypto (Node.js or a modern browser):

```js
async function verifyCartcord(reply, pinnedPublicJwk, pinnedKid, expected) {
  if (reply.algorithm !== "Ed25519" || reply.kid !== pinnedKid) return false;
  if (typeof reply.payload !== "string" || typeof reply.signature !== "string") return false;
  const key = await crypto.subtle.importKey("jwk", pinnedPublicJwk, "Ed25519", false, ["verify"]);
  const signature = Uint8Array.from(
    atob(reply.signature.replaceAll("-", "+").replaceAll("_", "/")),
    c => c.charCodeAt(0)
  );
  if (!await crypto.subtle.verify("Ed25519", key, signature, new TextEncoder().encode(reply.payload))) return false;
  const payload = JSON.parse(reply.payload);
  const now = Math.floor(Date.now() / 1000);
  return payload.valid === true && payload.iss === "https://cartcord.com" &&
    payload.product_id === expected.productId && payload.nonce === expected.nonce &&
    payload.key_fingerprint === expected.keyFingerprint &&
    payload.device_fingerprint === expected.deviceFingerprint &&
    Number.isInteger(payload.iat) && Number.isInteger(payload.exp) &&
    payload.iat <= now + 30 && payload.iat >= now - 330 &&
    payload.exp > now && payload.exp <= payload.iat + 300;
}
```

Compute `keyFingerprint` as lowercase hex SHA-256 of the trimmed license key.
Compute `deviceFingerprint` as SHA-256 of the lowercase product UUID, one NUL byte,
trimmed license key, one NUL byte, and trimmed device identifier (UTF-8).
Generate a fresh nonce for each request, compare it to that request, and do not reuse
an accepted response for another device, key or product. Catch parsing/crypto errors
and reject verification. A client can cache a verified result only until its signed
expiry; revocation therefore takes effect at the next online check, at most five
minutes later for a correctly implemented cache.

Device limits are enforced in a database transaction with a lock on the key. Full
refunds and access revocations invalidate associated keys. Sellers can permanently
revoke a key or clear device bindings from Delivery settings. A device reset does
not restore a revoked or refunded key. Only software that implements these checks
is protected; an API cannot prevent a modified client from bypassing local checks.

This release does not provide offline perpetual entitlements, automatic software
updates, or hardware ownership verification. Keep keys and requests out of logs.
The response intentionally does not disclose the buyer's email or Discord identity.
