On this page

Reseller API v1

Certware API

Plug instant delivery into your own storefront. Read the catalog at your partner price, create an order when a customer pays, and get the license keys back in the same response — charged to your Certware balance, minted on the spot.

The same keys you'd buy in the seller desk, the same balance, the same partner cut. Anything your storefront can do with an HTTP request, it can now do with Certware.

Base URL

Quick start

  1. Create a key in the seller desk under Developers. Give it only the scopes your integration needs; the secret is shown once.
  2. Check it works with GET /me, then pick a variant_id from GET /products.
  3. When a customer pays, call POST /orders with an Idempotency-Key. Hand them licenses[].key from the response.
  4. Optionally add a webhook endpoint in the same Developers page to hear about order.fulfilled, key redemptions, freezes and balance top-ups.
Check your key
curl "https://certware.xyz/api/v1/me" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Buy two keys
curl -X POST "https://certware.xyz/api/v1/orders" \
  -H "Authorization: Bearer $CERTWARE_API_KEY" \
  -H "Idempotency-Key: INV-1042" \
  -H "Content-Type: application/json" \
  -d '{
  "lines": [
    {
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "quantity": 2
    }
  ],
  "external_ref": "INV-1042",
  "customer_ref": "buyer@example.com"
}'

Conventions

  • JSON in and out, snake_case keys. Send Content-Type: application/json with request bodies; unknown body fields are rejected so typos don't pass silently.
  • Money is always an integer number of US cents: 1300 is $13.00. Never floats.
  • Timestamps are ISO 8601 in UTC, e.g. 2026-09-29T14:03:11.482Z. Ids carry a type prefix: ord_…, lic_…, var_…, shp_….
  • Every documented field is always present; empty values are null. Within v1 we only add fields and endpoints — ignore fields you don't recognise.
  • Lists look like { "object": "list", "data": [...], "has_more": false }, newest first. Use limit to size a page (and offset on /licenses).
  • Every response carries an X-Request-Id header (req_…). Include it when you contact us about a request.

Authentication

Authenticate every request with your API key as a bearer token: Authorization: Bearer cw_live_…. Create and revoke keys in the seller desk Developers. The full key is shown exactly once — we only store a hash — so put it in your server's secret store straight away.

Keys are server-side secrets. Never ship one in browser or app code: the API sends no CORS headers, so browsers can't call it anyway. Revoked or expired keys, and keys of a suspended shop, get 401 immediately.

Scopes

Each key carries scopes. An endpoint that needs a scope the key lacks answers 403 insufficient_scope and names it in details.required_scope. Scopes don't imply each other: licenses:write doesn't grant licenses:read.

catalog:read
List products and your partner prices.GET /products
orders:read
Read orders and the keys they delivered.GET /ordersGET /orders/{id}
orders:write
Create orders — spends your balance.POST /orders
licenses:read
Look up keys, who redeemed them, and their history.GET /licensesGET /licenses/{key_or_id}
balance:read
Read your balance and ledger.GET /balanceGET /ledger

GET /me works with any shop key, whatever its scopes.

Platform keys (staff automation)

Certware staff can automate operations — for example with an LLM agent — using platform keys created in the admin desk Developer API. Platform keys only work on /api/v1/admin/*, and shop keys never do.

A platform key acts as the staff member who created it, with that person's current role: if they're demoted the key loses what the new role can't do, and if they lose staff access or their account is disabled the key stops working (403). Changes made with a platform key are audited under both the staff member and the key.

admin:read
Platform keys: read shops, top-ups, the catalog and orders.
admin:write
Platform keys: approve or reject shops and top-ups, set detection, act on any key.

Idempotency

Networks fail after the money has moved, so POST /orders requires an Idempotency-Key header: a value unique to the purchase (your invoice or checkout id is ideal), up to 100 characters of A–Z a–z 0–9 _ . : -. Keys are scoped to your shop and never expire.

Same key, same body
You get the original order back — 200 with Idempotent-Replayed: true, the same keys, no second charge. Safe to retry after any timeout, network error, 429 or 500.
First request still running
409 idempotency_key_in_use with Retry-After and the order so far. Retry with the same key to collect the keys.
Same key, different body
409 conflict. Nothing happens; a key belongs to one purchase.
The order failed
502 fulfillment_failed: the order is failed and fully refunded, and the key stays bound to it. Place the order again with a new key.
Rejected before charging
400, 402, 403 and friends create nothing. Fix the problem (e.g. top up) and retry with the same key.

Errors

Errors use conventional HTTP statuses and always have the same body. Branch on error.code; message is for humans and may change.

402 · insufficient_funds
{
  "error": {
    "code": "insufficient_funds",
    "message": "Not enough balance. Top up first.",
    "details": {
      "available_cents": 1000,
      "required_cents": 2600
    },
    "request_id": "req_01m3pqjt2fdx7eanpfmkab9n9d"
  }
}
invalid_request
HTTP 400
Malformed JSON, a field failed validation (param names it), or a required header is missing. Fix the request.
invalid
HTTP 400
The request broke a business rule, e.g. a variant that's no longer sold. Fix the request.
unauthenticated
HTTP 401
The key is missing, invalid, expired or revoked, or its shop isn't active. Check the key.
insufficient_funds
HTTP 402
Your balance can't cover the order. details has available_cents and required_cents. Nothing was charged. Top up, then retry with the same Idempotency-Key.
insufficient_scope
HTTP 403
The key lacks the endpoint's scope (details.required_scope). Use a key with that scope.
forbidden
HTTP 403
Wrong kind of key for the endpoint, or a platform key whose creator no longer has the staff role it needs. Don't retry.
not_found
HTTP 404
No such resource, or it belongs to another shop. Don't retry.
conflict
HTTP 409
The resource is in the wrong state (e.g. unfreezing a key that isn't frozen), or an Idempotency-Key was reused with a different body. Don't retry.
idempotency_key_in_use
HTTP 409
The first request with this Idempotency-Key is still being fulfilled. The body includes order. Retry with the same key after Retry-After seconds.
payload_too_large
HTTP 413
Request bodies are limited to 64 KB. Don't retry.
rate_limited
HTTP 429
Too many requests. Retry after Retry-After seconds.
internal_error
HTTP 500
Our bug. It was logged with the request id. Retry with backoff (orders: same Idempotency-Key).
fulfillment_failed
HTTP 502
The license server failed to mint. The order is failed, refunded in full, and included as order. Retry with a new Idempotency-Key.
unavailable
HTTP 503
The license server can't be reached, or the product isn't linked to it yet. Retry later.

Rate limits

Over a limit you get 429 rate_limited with a Retry-After header in seconds. Successful responses report the per-key budget in X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets).

240 / minute
per API key
Every request.
60 / minute
per shop
POST /orders, across all of the shop's keys (every order mints keys on the license server).
120 / hour
per shop
HWID resets, freezes and unfreezes — shared with the same actions in the seller desk.

Account

The shop and key behind a request.

GET/api/v1/me

Get your shop and key

The shop this key belongs to, your partner cut, the prefix new keys are minted with, and this key's scopes. Works with any shop key, whatever its scopes — a good health check for your integration.

Shop keyany scope
Request
curl "https://certware.xyz/api/v1/me" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "shop",
  "id": "shp_01m3pqbaay35zv42d96qs5ae40",
  "slug": "north",
  "name": "North Supply",
  "status": "active",
  "tier": {
    "id": "tier_01m3pqd86ptzek9mqmm6nkr4wc",
    "name": "Certified",
    "discount_bps": 3500
  },
  "discount_bps": 3500,
  "branded_key_prefix": "ASX",
  "api_key": {
    "id": "key_01m3pqbj2d0zdn5tz7zteb8n96",
    "name": "Storefront",
    "prefix": "cw_live_4fQ9xk",
    "scopes": [
      "catalog:read",
      "orders:read",
      "orders:write",
      "licenses:read"
    ],
    "expires_at": null,
    "created_at": "2026-09-01T10:00:00.000Z"
  }
}

Errors: 401 · 403 · 429 · 500

Catalog

What you can sell, at your partner price.

GET/api/v1/products

List products you can order

Every live product with its active variants, priced at your partner cut. price_cents is exactly what POST /orders charges per key; retail_cents is the suggested retail price.

Check detection.status before selling: detected or updating means stop selling until it changes back to undetected.

Shop keycatalog:read
Request
curl "https://certware.xyz/api/v1/products" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "product",
      "id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "slug": "fortnite-external",
      "name": "Fortnite External",
      "short_name": "FN",
      "category": {
        "id": "cat_01m3pqd0f7vqw0s4ztqv40avcw",
        "slug": "battle-royale",
        "name": "Battle royale"
      },
      "detection": {
        "status": "undetected",
        "note": "",
        "updated_at": "2026-09-28T18:20:00.000Z"
      },
      "blurb": "Stream-proof external with a clean overlay, smooth aim and a menu we'll brand for your shop.",
      "features": [
        "Stream-proof overlay",
        "Humanised aim with smoothing",
        "Player + loot ESP"
      ],
      "requirements": [
        "Windows 10 or 11",
        "Intel or AMD CPU"
      ],
      "image_url": "https://certware.xyz/product-images/fortniteext.jpg",
      "variants": [
        {
          "id": "var_01m3pqc1hb308ktd63tjmatjfz",
          "name": "1 day",
          "duration_seconds": 86400,
          "retail_cents": 500,
          "price_cents": 325
        },
        {
          "id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
          "name": "7 days",
          "duration_seconds": 604800,
          "retail_cents": 2000,
          "price_cents": 1300
        },
        {
          "id": "var_01m3pqch096jtxcngwrtb8fq2r",
          "name": "30 days",
          "duration_seconds": 2592000,
          "retail_cents": 4000,
          "price_cents": 2600
        },
        {
          "id": "var_01m3pqcrqrctx8edmed4amyg37",
          "name": "Lifetime",
          "duration_seconds": null,
          "retail_cents": 15000,
          "price_cents": 9750
        }
      ]
    }
  ],
  "has_more": false
}

Errors: 401 · 403 · 429 · 500

Orders

Buy keys from your balance; they're minted and returned instantly.

POST/api/v1/orders

Create an order (instant delivery)

Charges your balance at your partner price, mints the keys and returns them in licenses[].key — hand them straight to your customer. Minting happens during the request, so give it a generous client timeout (60 seconds); if you still time out, retry with the same Idempotency-Key to collect the result.

The Idempotency-Key header is required. Use one key per purchase (your invoice id works well). Retrying with the same key and body never charges twice: you get the original order back with 200 and Idempotent-Replayed: true. Reusing a key with a different body is a 409 conflict.

If minting fails the order ends up failed, your balance is refunded in full and the response is 502 fulfillment_failed with the order attached; retry with a new Idempotency-Key. If the first request with a key is still running you get 409 idempotency_key_in_use with Retry-After; retry with the same key to collect the result.

Shop keyorders:writeIdempotency-Key required

Parameters

Idempotency-Key
headerstringrequired
Required. One value per purchase — your invoice id works well. Retrying with the same key and body returns the original order instead of charging again. Keys are scoped to your shop and never expire.
lines
bodyarray of objectrequired
What to buy: 1–20 lines, at most 200 keys in total.
lines[].variant_id
bodystringrequired
Variant to buy (var_…), from GET /products.
lines[].quantity
bodyintegerrequired
Keys of this variant, 1–100.
external_ref
bodystringoptional
Your own reference, e.g. your storefront invoice id. Echoed back and included in webhooks.
customer_ref
bodystringoptional
Your customer's email or Discord, for your records.
Request
curl -X POST "https://certware.xyz/api/v1/orders" \
  -H "Authorization: Bearer $CERTWARE_API_KEY" \
  -H "Idempotency-Key: INV-1042" \
  -H "Content-Type: application/json" \
  -d '{
  "lines": [
    {
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "quantity": 2
    }
  ],
  "external_ref": "INV-1042",
  "customer_ref": "buyer@example.com"
}'
Response · 201
{
  "object": "order",
  "id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "status": "fulfilled",
  "source": "api",
  "currency": "USD",
  "discount_bps": 3500,
  "retail_total_cents": 4000,
  "total_cents": 2600,
  "refunded_cents": 0,
  "external_ref": "INV-1042",
  "customer_ref": "buyer@example.com",
  "idempotency_key": "INV-1042",
  "failure": null,
  "created_at": "2026-09-29T14:03:11.482Z",
  "fulfilled_at": "2026-09-29T14:03:12.907Z",
  "items": [
    {
      "id": "oi_01m3pqdqnmpx0pa7gwszang95e",
      "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "product_name": "Fortnite External",
      "variant_name": "7 days",
      "duration_seconds": 604800,
      "quantity": 2,
      "unit_retail_cents": 2000,
      "unit_price_cents": 1300
    }
  ],
  "licenses": [
    {
      "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
      "key": "ASX-7KQM-P2VD-9XHT",
      "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
      "duration_seconds": 604800,
      "status": "active"
    },
    {
      "id": "lic_01m3pqe74jpmeyedng1r4rgsd8",
      "key": "ASX-4RWN-8GBC-TZ3E",
      "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
      "duration_seconds": 604800,
      "status": "active"
    }
  ]
}

Errors: 400 · 401 · 402 · 403 · 409 · 413 · 429 · 500 · 502 · 503

GET/api/v1/orders

List orders

Your orders, newest first, without their keys. Fetch GET /orders/{id} for the lines and keys of one order.

Page with starting_after: pass the id of the last order you received while has_more is true. To reconcile after a timeout, look an order up by the external_ref you sent when creating it.

Shop keyorders:read

Parameters

status
query"pending" | "fulfilling" | "fulfilled" | "failed" | "refunded" | "partially_refunded"optional
Only orders in this status.
external_ref
querystringoptional
Only the order(s) you created with this external_ref.
starting_after
querystringoptional
Cursor: the id of the last order on the previous page.
limit
queryintegeroptional
Page size, 1–100. Default 25.
Request
curl "https://certware.xyz/api/v1/orders?limit=25" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "order",
      "id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
      "status": "fulfilled",
      "source": "api",
      "currency": "USD",
      "discount_bps": 3500,
      "retail_total_cents": 4000,
      "total_cents": 2600,
      "refunded_cents": 0,
      "external_ref": "INV-1042",
      "customer_ref": "buyer@example.com",
      "idempotency_key": "INV-1042",
      "failure": null,
      "created_at": "2026-09-29T14:03:11.482Z",
      "fulfilled_at": "2026-09-29T14:03:12.907Z"
    }
  ],
  "has_more": false
}

Errors: 400 · 401 · 403 · 429 · 500

GET/api/v1/orders/{id}

Get an order

One of your orders with its lines and delivered keys. Other shops' orders are always 404.

Shop keyorders:read

Parameters

id
pathstringrequired
—
Request
curl "https://certware.xyz/api/v1/orders/ord_01m3pqdfy5zdyem2682rr5n1ef" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "order",
  "id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "status": "fulfilled",
  "source": "api",
  "currency": "USD",
  "discount_bps": 3500,
  "retail_total_cents": 4000,
  "total_cents": 2600,
  "refunded_cents": 0,
  "external_ref": "INV-1042",
  "customer_ref": "buyer@example.com",
  "idempotency_key": "INV-1042",
  "failure": null,
  "created_at": "2026-09-29T14:03:11.482Z",
  "fulfilled_at": "2026-09-29T14:03:12.907Z",
  "items": [
    {
      "id": "oi_01m3pqdqnmpx0pa7gwszang95e",
      "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "product_name": "Fortnite External",
      "variant_name": "7 days",
      "duration_seconds": 604800,
      "quantity": 2,
      "unit_retail_cents": 2000,
      "unit_price_cents": 1300
    }
  ],
  "licenses": [
    {
      "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
      "key": "ASX-7KQM-P2VD-9XHT",
      "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
      "duration_seconds": 604800,
      "status": "active"
    },
    {
      "id": "lic_01m3pqe74jpmeyedng1r4rgsd8",
      "key": "ASX-4RWN-8GBC-TZ3E",
      "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
      "duration_seconds": 604800,
      "status": "active"
    }
  ]
}

Errors: 400 · 401 · 403 · 404 · 429 · 500

Licenses

Look up the keys you've sold and manage them for your customers.

GET/api/v1/licenses

List licenses

Keys you've bought, newest first, with each key's claim (the customer's Discord) when it has been redeemed. Page with limit and offset.

Shop keylicenses:read

Parameters

product_id
querystringoptional
Only keys for this product.
status
query"active" | "frozen" | "refunded" | "revoked"optional
Only keys in this status.
claimed
query"true" | "false"optional
true: only keys a customer has redeemed. false: only unredeemed keys.
q
querystringoptional
Search: part of a key, an order id, part of a Discord username, or an exact Discord id.
limit
queryintegeroptional
Page size, 1–100. Default 50.
offset
queryintegeroptional
Items to skip. Default 0.
Request
curl "https://certware.xyz/api/v1/licenses?claimed=true&limit=50" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "license",
      "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
      "key": "ASX-7KQM-P2VD-9XHT",
      "status": "active",
      "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "product_name": "Fortnite External",
      "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
      "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
      "source": "order",
      "duration_seconds": 604800,
      "activation": {
        "status": "active",
        "hwid_bound": true,
        "expires_at": "2026-10-06T15:10:44.000Z",
        "synced_at": "2026-09-29T15:12:02.311Z"
      },
      "frozen_at": null,
      "frozen_reason": null,
      "claim": {
        "discord_id": "284726958109982720",
        "discord_username": "mika",
        "claimed_at": "2026-09-29T15:09:37.120Z"
      },
      "created_at": "2026-09-29T14:03:12.907Z"
    }
  ],
  "has_more": false
}

Errors: 400 · 401 · 403 · 429 · 500

GET/api/v1/licenses/{key_or_id}

Get a license

Look up one of your keys by its id or the key itself (case-insensitive). Includes who redeemed it (claim), its last known activation state and its recent history.

activation is a snapshot from the license server, refreshed whenever the key is acted on; synced_at says how fresh it is. Keys belonging to other shops are always 404.

Shop keylicenses:read

Parameters

key_or_id
pathstringrequired
A license id (lic_…) or the key itself (case-insensitive).
Request
curl "https://certware.xyz/api/v1/licenses/ASX-7KQM-P2VD-9XHT" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "license",
  "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
  "key": "ASX-7KQM-P2VD-9XHT",
  "status": "active",
  "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "product_name": "Fortnite External",
  "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
  "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
  "source": "order",
  "duration_seconds": 604800,
  "activation": {
    "status": "active",
    "hwid_bound": true,
    "expires_at": "2026-10-06T15:10:44.000Z",
    "synced_at": "2026-09-29T15:12:02.311Z"
  },
  "frozen_at": null,
  "frozen_reason": null,
  "claim": {
    "discord_id": "284726958109982720",
    "discord_username": "mika",
    "claimed_at": "2026-09-29T15:09:37.120Z"
  },
  "created_at": "2026-09-29T14:03:12.907Z",
  "timeline": [
    {
      "id": "lev_01m3pqepkgj77ccd43093ye6k1",
      "type": "claimed",
      "actor_type": "customer",
      "data": {
        "discord_id": "284726958109982720"
      },
      "created_at": "2026-09-29T15:09:37.120Z"
    },
    {
      "id": "lev_01m3pqeew14shqr3f76kt28xh5",
      "type": "minted",
      "actor_type": "system",
      "data": {
        "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef"
      },
      "created_at": "2026-09-29T14:03:12.907Z"
    }
  ]
}

Errors: 400 · 401 · 403 · 404 · 429 · 500

POST/api/v1/licenses/{key_or_id}/reset-hwid

Reset a key's HWID

Unbinds the key from the machine it was activated on, so your customer can activate it on a new PC. No cooldown for sellers (customers resetting on the download hub have one). Only active keys can be reset. Sends license.hwid_reset.

Shop keylicenses:write

Parameters

key_or_id
pathstringrequired
A license id (lic_…) or the key itself (case-insensitive).
Request
curl -X POST "https://certware.xyz/api/v1/licenses/ASX-7KQM-P2VD-9XHT/reset-hwid" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "license",
  "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
  "key": "ASX-7KQM-P2VD-9XHT",
  "status": "active",
  "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "product_name": "Fortnite External",
  "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
  "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
  "source": "order",
  "duration_seconds": 604800,
  "activation": {
    "status": "active",
    "hwid_bound": false,
    "expires_at": "2026-10-06T15:10:44.000Z",
    "synced_at": "2026-09-29T15:12:02.311Z"
  },
  "frozen_at": null,
  "frozen_reason": null,
  "claim": {
    "discord_id": "284726958109982720",
    "discord_username": "mika",
    "claimed_at": "2026-09-29T15:09:37.120Z"
  },
  "created_at": "2026-09-29T14:03:12.907Z"
}

Errors: 400 · 401 · 403 · 404 · 409 · 429 · 500 · 503

POST/api/v1/licenses/{key_or_id}/freeze

Freeze a key

Stops the key from working until you unfreeze it — for chargebacks, disputes or abuse. Active keys are paused (their remaining time is kept); unused keys are blocked. Freezing a frozen key is a no-op. Sends license.frozen.

Shop keylicenses:write

Parameters

key_or_id
pathstringrequired
A license id (lic_…) or the key itself (case-insensitive).
reason
bodystringrequired
Why the key is frozen. Stored on the key and sent in the license.frozen webhook.
Request
curl -X POST "https://certware.xyz/api/v1/licenses/ASX-7KQM-P2VD-9XHT/freeze" \
  -H "Authorization: Bearer $CERTWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Chargeback opened"
}'
Response · 200
{
  "object": "license",
  "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
  "key": "ASX-7KQM-P2VD-9XHT",
  "status": "frozen",
  "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "product_name": "Fortnite External",
  "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
  "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
  "source": "order",
  "duration_seconds": 604800,
  "activation": {
    "status": "active",
    "hwid_bound": true,
    "expires_at": "2026-10-06T15:10:44.000Z",
    "synced_at": "2026-09-29T15:12:02.311Z"
  },
  "frozen_at": "2026-09-30T08:41:19.004Z",
  "frozen_reason": "Chargeback opened",
  "claim": {
    "discord_id": "284726958109982720",
    "discord_username": "mika",
    "claimed_at": "2026-09-29T15:09:37.120Z"
  },
  "created_at": "2026-09-29T14:03:12.907Z"
}

Errors: 400 · 401 · 403 · 404 · 409 · 413 · 429 · 500 · 503

POST/api/v1/licenses/{key_or_id}/unfreeze

Unfreeze a key

Reverses a freeze exactly as it was applied: paused keys resume with their remaining time, blocked keys go back to unused. Sends license.unfrozen.

Shop keylicenses:write

Parameters

key_or_id
pathstringrequired
A license id (lic_…) or the key itself (case-insensitive).
Request
curl -X POST "https://certware.xyz/api/v1/licenses/ASX-7KQM-P2VD-9XHT/unfreeze" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "license",
  "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
  "key": "ASX-7KQM-P2VD-9XHT",
  "status": "active",
  "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "product_name": "Fortnite External",
  "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
  "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
  "source": "order",
  "duration_seconds": 604800,
  "activation": {
    "status": "active",
    "hwid_bound": true,
    "expires_at": "2026-10-06T15:10:44.000Z",
    "synced_at": "2026-09-29T15:12:02.311Z"
  },
  "frozen_at": null,
  "frozen_reason": null,
  "claim": {
    "discord_id": "284726958109982720",
    "discord_username": "mika",
    "claimed_at": "2026-09-29T15:09:37.120Z"
  },
  "created_at": "2026-09-29T14:03:12.907Z"
}

Errors: 400 · 401 · 403 · 404 · 409 · 429 · 500 · 503

Balance

Your prepaid balance and its ledger.

GET/api/v1/balance

Get your balance

Your spendable balance. Top up from the seller desk (Wallet); approved top-ups send balance.credited.

Shop keybalance:read
Request
curl "https://certware.xyz/api/v1/balance" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "balance",
  "available_cents": 48700,
  "currency": "USD"
}

Errors: 401 · 403 · 429 · 500

GET/api/v1/ledger

List ledger entries

Every movement of your balance, newest first: top-ups, orders, refunds (including automatic refunds of failed orders), tier upgrades and staff adjustments. balance_after_cents lets you reconcile.

Shop keybalance:read

Parameters

kind
query"topup" | "order" | "refund" | "tier_upgrade" | "referral_credit" | "custom_work" | "custom_work_refund" | "adjustment"optional
Only entries of this kind.
limit
queryintegeroptional
Page size, 1–100. Default 50.
Request
curl "https://certware.xyz/api/v1/ledger?limit=2" \
  -H "Authorization: Bearer $CERTWARE_API_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "ledger_entry",
      "id": "le_01m3pqfnhcm8e4vmm9qjjga10p",
      "kind": "order",
      "amount_cents": -2600,
      "balance_after_cents": 48700,
      "ref_type": "order",
      "ref_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
      "description": "Order ord_01m3pqdfy5zdyem2682rr5n1ef: 2× Fortnite External 7 days",
      "created_at": "2026-09-29T14:03:11.482Z"
    },
    {
      "object": "ledger_entry",
      "id": "le_01m3pqfdsx6f6m55b2q0vdtzx1",
      "kind": "topup",
      "amount_cents": 50000,
      "balance_after_cents": 51300,
      "ref_type": "topup_request",
      "ref_id": "top_01m3pqf62e0z7k7s39f746dmvy",
      "description": "Top-up via crypto (0x9f3c4b7de21a8c5f)",
      "created_at": "2026-09-28T09:31:07.215Z"
    }
  ],
  "has_more": true
}

Errors: 400 · 401 · 403 · 429 · 500

Platform (staff)

Staff automation with platform keys: review shops and top-ups, manage the catalog and keys.

These need a platform key (see Authentication). Each endpoint also requires the key's creator to hold a staff role allowed to do the same thing in the admin desk.

GET/api/v1/admin/shops

List shops

Shops newest first, with tier and balance. Filter status=pending for the application queue.

Platform keyadmin:readStaff role: owner, admin, developer, support

Parameters

status
query"pending" | "active" | "rejected" | "suspended"optional
Only shops in this status.
q
querystringoptional
Search name, slug or contact email (partial), or an exact shop id.
limit
queryintegeroptional
Page size, 1–100. Default 50.
Request
curl "https://certware.xyz/api/v1/admin/shops?status=active" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "shop",
      "id": "shp_01m3pqk9hd4masamk25cms2z9t",
      "slug": "polar",
      "name": "Polar Keys",
      "status": "active",
      "tier": {
        "id": "tier_01m3pqd86ptzek9mqmm6nkr4wc",
        "name": "Certified"
      },
      "contact_email": "owner@polarkeys.gg",
      "website_url": "https://polarkeys.gg/",
      "balance_cents": 0,
      "lifetime_spend_cents": 0,
      "custom_discount_bps": null,
      "referral_bonus_bps": 0,
      "key_prefix": null,
      "suspended_reason": null,
      "approved_at": "2026-09-29T14:20:00.000Z",
      "created_at": "2026-09-27T09:12:44.000Z"
    }
  ],
  "has_more": false
}

Errors: 400 · 401 · 403 · 429 · 500

POST/api/v1/admin/shops/{id}/approve

Approve a shop application

Opens a pending shop's desk on the given tier (or the default tier) and emails the owner. Only pending shops can be approved.

Platform keyadmin:writeStaff role: owner, admin

Parameters

id
pathstringrequired
—
tier_id
bodystringoptional
Tier to start the shop on. Defaults to the default tier.
note
bodystringoptional
Internal review note.
Request
curl -X POST "https://certware.xyz/api/v1/admin/shops/shp_01m3pqk9hd4masamk25cms2z9t/approve" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "tier_id": "tier_01m3pqd86ptzek9mqmm6nkr4wc",
  "note": "Strong storefront, low chargebacks."
}'
Response · 200
{
  "object": "shop",
  "id": "shp_01m3pqk9hd4masamk25cms2z9t",
  "slug": "polar",
  "name": "Polar Keys",
  "status": "active",
  "tier": {
    "id": "tier_01m3pqd86ptzek9mqmm6nkr4wc",
    "name": "Certified"
  },
  "contact_email": "owner@polarkeys.gg",
  "website_url": "https://polarkeys.gg/",
  "balance_cents": 0,
  "lifetime_spend_cents": 0,
  "custom_discount_bps": null,
  "referral_bonus_bps": 0,
  "key_prefix": null,
  "suspended_reason": null,
  "approved_at": "2026-09-29T14:20:00.000Z",
  "created_at": "2026-09-27T09:12:44.000Z"
}

Errors: 400 · 401 · 403 · 404 · 409 · 413 · 429 · 500

POST/api/v1/admin/shops/{id}/reject

Reject a shop application

Declines a pending application and emails the applicant the reason.

Platform keyadmin:writeStaff role: owner, admin

Parameters

id
pathstringrequired
—
reason
bodystringrequired
Shown to the applicant in the rejection email.
Request
curl -X POST "https://certware.xyz/api/v1/admin/shops/shp_01m3pqk9hd4masamk25cms2z9t/reject" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "We couldn'\''t verify the storefront. Apply again once it'\''s live."
}'
Response · 200
{
  "object": "shop",
  "id": "shp_01m3pqk9hd4masamk25cms2z9t",
  "slug": "polar",
  "name": "Polar Keys",
  "status": "rejected",
  "tier": null,
  "contact_email": "owner@polarkeys.gg",
  "website_url": "https://polarkeys.gg/",
  "balance_cents": 0,
  "lifetime_spend_cents": 0,
  "custom_discount_bps": null,
  "referral_bonus_bps": 0,
  "key_prefix": null,
  "suspended_reason": null,
  "approved_at": null,
  "created_at": "2026-09-27T09:12:44.000Z"
}

Errors: 400 · 401 · 403 · 404 · 409 · 413 · 429 · 500

GET/api/v1/admin/topups

List top-up requests

Balance top-ups filed by shops. With status=pending the queue comes oldest first (review order); otherwise newest first.

Platform keyadmin:readStaff role: owner, admin, developer, support

Parameters

status
query"pending" | "approved" | "rejected" | "cancelled" | "refunded" | "disputed"optional
Only top-ups in this status.
shop_id
querystringoptional
Only this shop's top-ups.
limit
queryintegeroptional
Page size, 1–100. Default 50.
Request
curl "https://certware.xyz/api/v1/admin/topups?status=pending" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "topup",
      "id": "top_01m3pqf62e0z7k7s39f746dmvy",
      "shop": {
        "id": "shp_01m3pqbaay35zv42d96qs5ae40",
        "name": "North Supply",
        "slug": "north"
      },
      "amount_cents": 50000,
      "method": "crypto",
      "reference": "0x9f3c4b7de21a8c5f",
      "note": "USDT on Tron",
      "status": "pending",
      "review_note": null,
      "reviewed_at": null,
      "created_at": "2026-09-28T09:15:52.640Z"
    }
  ],
  "has_more": false
}

Errors: 400 · 401 · 403 · 429 · 500

POST/api/v1/admin/topups/{id}/approve

Approve a top-up

Credits the amount to the shop's balance, emails the shop and sends balance.credited. Verify the payment reference first. Only pending top-ups can be reviewed, so a retry after success is a 409.

Platform keyadmin:writeStaff role: owner, admin

Parameters

id
pathstringrequired
—
note
bodystringoptional
Optional note, kept on the top-up.
Request
curl -X POST "https://certware.xyz/api/v1/admin/topups/top_01m3pqf62e0z7k7s39f746dmvy/approve" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "note": "Verified on-chain."
}'
Response · 200
{
  "object": "topup",
  "id": "top_01m3pqf62e0z7k7s39f746dmvy",
  "shop": {
    "id": "shp_01m3pqbaay35zv42d96qs5ae40",
    "name": "North Supply",
    "slug": "north"
  },
  "amount_cents": 50000,
  "method": "crypto",
  "reference": "0x9f3c4b7de21a8c5f",
  "note": "USDT on Tron",
  "status": "approved",
  "review_note": "Verified on-chain.",
  "reviewed_at": "2026-09-28T09:31:07.215Z",
  "created_at": "2026-09-28T09:15:52.640Z"
}

Errors: 400 · 401 · 403 · 404 · 409 · 413 · 429 · 500

POST/api/v1/admin/topups/{id}/reject

Reject a top-up

Declines a pending top-up without moving money and tells the shop why.

Platform keyadmin:writeStaff role: owner, admin

Parameters

id
pathstringrequired
—
reason
bodystringrequired
Shown to the shop in the rejection notice and email.
Request
curl -X POST "https://certware.xyz/api/v1/admin/topups/top_01m3pqks0b5j4qxt0tc4se9w2r/reject" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "No matching transfer found for that reference."
}'
Response · 200
{
  "object": "topup",
  "id": "top_01m3pqks0b5j4qxt0tc4se9w2r",
  "shop": {
    "id": "shp_01m3pqbaay35zv42d96qs5ae40",
    "name": "North Supply",
    "slug": "north"
  },
  "amount_cents": 50000,
  "method": "crypto",
  "reference": "0x9f3c4b7de21a8c5f",
  "note": "USDT on Tron",
  "status": "rejected",
  "review_note": "No matching transfer found for that reference.",
  "reviewed_at": "2026-09-28T09:31:07.215Z",
  "created_at": "2026-09-28T09:15:52.640Z"
}

Errors: 400 · 401 · 403 · 404 · 409 · 413 · 429 · 500

GET/api/v1/admin/products

List all products

The whole catalog in display order, including drafts, disabled products and retired variants.

Platform keyadmin:readStaff role: owner, admin, developer, support
Request
curl "https://certware.xyz/api/v1/admin/products" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "product",
      "id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
      "slug": "fortnite-external",
      "name": "Fortnite External",
      "short_name": "FN",
      "status": "live",
      "category": {
        "id": "cat_01m3pqd0f7vqw0s4ztqv40avcw",
        "slug": "battle-royale",
        "name": "Battle royale"
      },
      "detection": {
        "status": "undetected",
        "note": "",
        "updated_at": "2026-09-28T18:20:00.000Z"
      },
      "evora_app_id": "3f6c2a9e-8b1d-4c7e-9a2f-5d8e1b6c4a70",
      "evora_level": 1,
      "variants": [
        {
          "id": "var_01m3pqc1hb308ktd63tjmatjfz",
          "name": "1 day",
          "duration_seconds": 86400,
          "retail_cents": 500,
          "active": true,
          "sort_order": 0
        },
        {
          "id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
          "name": "7 days",
          "duration_seconds": 604800,
          "retail_cents": 2000,
          "active": true,
          "sort_order": 1
        },
        {
          "id": "var_01m3pqch096jtxcngwrtb8fq2r",
          "name": "30 days",
          "duration_seconds": 2592000,
          "retail_cents": 4000,
          "active": true,
          "sort_order": 2
        },
        {
          "id": "var_01m3pqcrqrctx8edmed4amyg37",
          "name": "Lifetime",
          "duration_seconds": null,
          "retail_cents": 15000,
          "active": true,
          "sort_order": 3
        }
      ],
      "created_at": "2026-09-01T09:00:00.000Z",
      "updated_at": "2026-09-28T18:20:00.000Z"
    }
  ],
  "has_more": false
}

Errors: 401 · 403 · 429 · 500

POST/api/v1/admin/products/{id}/detection

Set a product's detection status

Updates the status resellers watch before selling and notifies every active shop's desk with your note.

Platform keyadmin:writeStaff role: owner, admin, developer

Parameters

id
pathstringrequired
—
detection
body"undetected" | "testing" | "updating" | "detected" | "unknown"required
—
note
bodystringrequired
What changed. Broadcast to every active shop's desk.
Request
curl -X POST "https://certware.xyz/api/v1/admin/products/prod_01m3pqbssww1nqmgmmf2k0yheb/detection" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "detection": "updating",
  "note": "Game patch today; build update in progress. Pause sales."
}'
Response · 200
{
  "object": "product",
  "id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "slug": "fortnite-external",
  "name": "Fortnite External",
  "short_name": "FN",
  "status": "live",
  "category": {
    "id": "cat_01m3pqd0f7vqw0s4ztqv40avcw",
    "slug": "battle-royale",
    "name": "Battle royale"
  },
  "detection": {
    "status": "updating",
    "note": "Game patch today; build update in progress. Pause sales.",
    "updated_at": "2026-09-30T08:41:19.004Z"
  },
  "evora_app_id": "3f6c2a9e-8b1d-4c7e-9a2f-5d8e1b6c4a70",
  "evora_level": 1,
  "variants": [
    {
      "id": "var_01m3pqc1hb308ktd63tjmatjfz",
      "name": "1 day",
      "duration_seconds": 86400,
      "retail_cents": 500,
      "active": true,
      "sort_order": 0
    },
    {
      "id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
      "name": "7 days",
      "duration_seconds": 604800,
      "retail_cents": 2000,
      "active": true,
      "sort_order": 1
    },
    {
      "id": "var_01m3pqch096jtxcngwrtb8fq2r",
      "name": "30 days",
      "duration_seconds": 2592000,
      "retail_cents": 4000,
      "active": true,
      "sort_order": 2
    },
    {
      "id": "var_01m3pqcrqrctx8edmed4amyg37",
      "name": "Lifetime",
      "duration_seconds": null,
      "retail_cents": 15000,
      "active": true,
      "sort_order": 3
    }
  ],
  "created_at": "2026-09-01T09:00:00.000Z",
  "updated_at": "2026-09-30T08:41:19.004Z"
}

Errors: 400 · 401 · 403 · 404 · 413 · 429 · 500

GET/api/v1/admin/orders

List orders across shops

Most recent orders platform-wide, newest first. Filter status=failed to watch fulfillment problems.

Platform keyadmin:readStaff role: owner, admin, developer, support

Parameters

shop_id
querystringoptional
Only this shop's orders.
status
query"pending" | "fulfilling" | "fulfilled" | "failed" | "refunded" | "partially_refunded"optional
Only orders in this status.
limit
queryintegeroptional
Page size, 1–100. Default 50.
Request
curl "https://certware.xyz/api/v1/admin/orders?status=failed" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"
Response · 200
{
  "object": "list",
  "data": [
    {
      "object": "order",
      "id": "ord_01m3pqkh8wsvh8p5mfy45n04zd",
      "status": "failed",
      "source": "api",
      "currency": "USD",
      "discount_bps": 3500,
      "retail_total_cents": 4000,
      "total_cents": 2600,
      "refunded_cents": 0,
      "external_ref": "INV-1043",
      "customer_ref": "buyer@example.com",
      "idempotency_key": "INV-1043",
      "failure": {
        "code": "evora_upstream",
        "message": "License server error: upstream timeout"
      },
      "created_at": "2026-09-29T14:03:11.482Z",
      "fulfilled_at": null,
      "shop_id": "shp_01m3pqbaay35zv42d96qs5ae40"
    }
  ],
  "has_more": false
}

Errors: 400 · 401 · 403 · 429 · 500

POST/api/v1/admin/licenses/{key_or_id}/freeze

Freeze any key

Freezes a key regardless of the shop that owns it. Same behavior as the shop endpoint; the owning shop gets license.frozen.

Platform keyadmin:writeStaff role: owner, admin, developer, support

Parameters

key_or_id
pathstringrequired
A license id (lic_…) or the key itself (case-insensitive).
reason
bodystringrequired
Why the key is frozen. Stored on the key and sent in the license.frozen webhook.
Request
curl -X POST "https://certware.xyz/api/v1/admin/licenses/ASX-7KQM-P2VD-9XHT/freeze" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Fraud review"
}'
Response · 200
{
  "object": "license",
  "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
  "key": "ASX-7KQM-P2VD-9XHT",
  "status": "frozen",
  "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "product_name": "Fortnite External",
  "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
  "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
  "source": "order",
  "duration_seconds": 604800,
  "activation": {
    "status": "active",
    "hwid_bound": true,
    "expires_at": "2026-10-06T15:10:44.000Z",
    "synced_at": "2026-09-29T15:12:02.311Z"
  },
  "frozen_at": "2026-09-30T08:41:19.004Z",
  "frozen_reason": "Fraud review",
  "claim": {
    "discord_id": "284726958109982720",
    "discord_username": "mika",
    "claimed_at": "2026-09-29T15:09:37.120Z"
  },
  "created_at": "2026-09-29T14:03:12.907Z",
  "shop_id": "shp_01m3pqbaay35zv42d96qs5ae40"
}

Errors: 400 · 401 · 403 · 404 · 409 · 413 · 429 · 500 · 503

POST/api/v1/admin/licenses/{key_or_id}/unfreeze

Unfreeze any key

Reverses a freeze on any shop's key. The owning shop gets license.unfrozen.

Platform keyadmin:writeStaff role: owner, admin, developer, support

Parameters

key_or_id
pathstringrequired
A license id (lic_…) or the key itself (case-insensitive).
Request
curl -X POST "https://certware.xyz/api/v1/admin/licenses/ASX-7KQM-P2VD-9XHT/unfreeze" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"
Response · 200
{
  "object": "license",
  "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
  "key": "ASX-7KQM-P2VD-9XHT",
  "status": "active",
  "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "product_name": "Fortnite External",
  "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
  "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
  "source": "order",
  "duration_seconds": 604800,
  "activation": {
    "status": "active",
    "hwid_bound": true,
    "expires_at": "2026-10-06T15:10:44.000Z",
    "synced_at": "2026-09-29T15:12:02.311Z"
  },
  "frozen_at": null,
  "frozen_reason": null,
  "claim": {
    "discord_id": "284726958109982720",
    "discord_username": "mika",
    "claimed_at": "2026-09-29T15:09:37.120Z"
  },
  "created_at": "2026-09-29T14:03:12.907Z",
  "shop_id": "shp_01m3pqbaay35zv42d96qs5ae40"
}

Errors: 400 · 401 · 403 · 404 · 409 · 429 · 500 · 503

POST/api/v1/admin/licenses/{key_or_id}/reset-hwid

Reset any key's HWID

Unbinds any shop's key from its machine. The owning shop gets license.hwid_reset.

Platform keyadmin:writeStaff role: owner, admin, developer, support

Parameters

key_or_id
pathstringrequired
A license id (lic_…) or the key itself (case-insensitive).
Request
curl -X POST "https://certware.xyz/api/v1/admin/licenses/ASX-7KQM-P2VD-9XHT/reset-hwid" \
  -H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"
Response · 200
{
  "object": "license",
  "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
  "key": "ASX-7KQM-P2VD-9XHT",
  "status": "active",
  "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
  "product_name": "Fortnite External",
  "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
  "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
  "order_item_id": "oi_01m3pqdqnmpx0pa7gwszang95e",
  "source": "order",
  "duration_seconds": 604800,
  "activation": {
    "status": "active",
    "hwid_bound": false,
    "expires_at": "2026-10-06T15:10:44.000Z",
    "synced_at": "2026-09-29T15:12:02.311Z"
  },
  "frozen_at": null,
  "frozen_reason": null,
  "claim": {
    "discord_id": "284726958109982720",
    "discord_username": "mika",
    "claimed_at": "2026-09-29T15:09:37.120Z"
  },
  "created_at": "2026-09-29T14:03:12.907Z",
  "shop_id": "shp_01m3pqbaay35zv42d96qs5ae40"
}

Errors: 400 · 401 · 403 · 404 · 409 · 429 · 500 · 503

Webhooks

Instead of polling, let Certware tell you when something happens. Add HTTPS endpoints in the seller desk Developers (up to 10 per shop) and choose which events each receives. Every endpoint gets its own signing secret (whsec_…), which you can reveal or rotate there. Use Send test to fire a ping.

Each delivery is a POST with a JSON body. The body is the same for every retry of an event; created is when the event happened.

POST https://yourshop.com/webhooks/certware
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "order.fulfilled",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
    "external_ref": "INV-1042",
    "total_cents": 2600,
    "licenses": [
      {
        "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
        "key": "ASX-7KQM-P2VD-9XHT",
        "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
        "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
        "duration_seconds": 604800
      },
      {
        "id": "lic_01m3pqe74jpmeyedng1r4rgsd8",
        "key": "ASX-4RWN-8GBC-TZ3E",
        "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
        "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
        "duration_seconds": 604800
      }
    ]
  }
}

Headers

Certware-Event
The event type, e.g. order.fulfilled.
Certware-Event-Id
The event id (evt_…), same as id in the body. Deliveries are at-least-once: dedupe on it.
Certware-Delivery
This delivery's id (whd_…). Stays the same across retries of the same delivery.
Certware-Signature
t=<unix seconds>,v1=<hex HMAC-SHA256> of <t>.<raw body>, keyed with the endpoint's signing secret (whsec_…). Verify it before trusting the body.

Verifying signatures

Always verify Certware-Signature before trusting a delivery. It is t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body> keyed with your endpoint's whole signing secret (including whsec_). Compute it over the exact bytes you received — re-serialising parsed JSON changes them — compare in constant time, and reject timestamps more than 5 minutes from your clock so a captured delivery can't be replayed later.

verify-certware-webhook.mjs (Node.js 18+)
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 5 * 60;

/**
 * Verify a Certware webhook and return the parsed event.
 * rawBody: the exact request body as a string (read it before any JSON parsing).
 * secret: the endpoint's signing secret, including the "whsec_" prefix.
 */
export function verifyCertwareWebhook(rawBody, signatureHeader, secret) {
  let timestamp = NaN;
  const signatures = [];
  for (const part of String(signatureHeader ?? "").split(",")) {
    const [name, value = ""] = part.trim().split("=", 2);
    if (name === "t") timestamp = Number(value);
    else if (name === "v1" && value) signatures.push(value);
  }
  if (!Number.isInteger(timestamp) || signatures.length === 0) {
    throw new Error("Malformed Certware-Signature header");
  }
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) {
    throw new Error("Timestamp outside the 5-minute tolerance (possible replay)");
  }
  const expected = Buffer.from(
    createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex")
  );
  const valid = signatures.some((signature) => {
    const received = Buffer.from(signature);
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
  if (!valid) throw new Error("Invalid Certware webhook signature");
  return JSON.parse(rawBody);
}
Using it
// Express: keep the raw body for verification.
app.post("/webhooks/certware", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = verifyCertwareWebhook(
      req.body.toString("utf8"),
      req.get("Certware-Signature"),
      process.env.CERTWARE_WEBHOOK_SECRET
    );
  } catch {
    return res.sendStatus(400);
  }
  res.sendStatus(200); // acknowledge within 10 seconds, then do the work
  if (alreadyHandled(event.id)) return; // deliveries are at-least-once
  if (event.type === "order.fulfilled") deliverKeys(event.data.external_ref, event.data.licenses);
});

// Next.js route handler: same idea.
export async function POST(request) {
  let event;
  try {
    event = verifyCertwareWebhook(
      await request.text(),
      request.headers.get("certware-signature"),
      process.env.CERTWARE_WEBHOOK_SECRET
    );
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }
  // …dedupe on event.id, then handle event.type…
  return new Response(null, { status: 204 });
}

Delivery and retries

  • Answer with any 2xx within 10 seconds. Anything else — other statuses, redirects (they aren't followed), timeouts — counts as a failure. Acknowledge first, then do slow work.
  • Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h — 8 attempts in total.
  • Delivery is at-least-once and events can arrive out of order. Dedupe on the event id (also in Certware-Event-Id), and fetch current state from the API when order matters.
  • After 25 consecutive failed attempts the endpoint is disabled and your desk is notified. Fix it and re-enable it in Developers.

Events

order.fulfilledOrder fulfilled

Keys were minted and delivered. Fires for every order — desk, API and storefront — so it's the one place to deliver keys from if you fulfil asynchronously.

order.fulfilled payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "order.fulfilled",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
    "external_ref": "INV-1042",
    "total_cents": 2600,
    "licenses": [
      {
        "id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
        "key": "ASX-7KQM-P2VD-9XHT",
        "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
        "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
        "duration_seconds": 604800
      },
      {
        "id": "lic_01m3pqe74jpmeyedng1r4rgsd8",
        "key": "ASX-4RWN-8GBC-TZ3E",
        "product_id": "prod_01m3pqbssww1nqmgmmf2k0yheb",
        "variant_id": "var_01m3pqc98tsk8rj3dh7x3xf91t",
        "duration_seconds": 604800
      }
    ]
  }
}
order.failedOrder failed

Minting failed and the order was refunded in full to your balance.

order.failed payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "order.failed",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "order_id": "ord_01m3pqkh8wsvh8p5mfy45n04zd",
    "external_ref": "INV-1043",
    "code": "evora_upstream",
    "message": "License server error: upstream timeout"
  }
}
order.refundedOrder refunded

Staff refunded some or all keys of an order. The keys stop working and the amount goes back to your balance.

order.refunded payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "order.refunded",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "order_id": "ord_01m3pqdfy5zdyem2682rr5n1ef",
    "refund_id": "rf_01m3pqjjb0vkc2tjknb6f8kp8a",
    "amount_cents": 1300,
    "license_ids": [
      "lic_01m3pqe74jpmeyedng1r4rgsd8"
    ]
  }
}
license.claimedKey redeemed by a customer

A customer redeemed the key on the download hub; it is now bound to their Discord account for good.

license.claimed payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "license.claimed",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "license_id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
    "key": "ASX-7KQM-P2VD-9XHT",
    "discord_id": "284726958109982720",
    "discord_username": "mika"
  }
}
license.hwid_resetHWID reset

The key was unbound from its machine. by is customer when the customer did it on the download hub; it's absent when you (desk or API) or staff did.

license.hwid_reset payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "license.hwid_reset",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "license_id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
    "key": "ASX-7KQM-P2VD-9XHT",
    "by": "customer"
  }
}
license.frozenKey frozen

The key stopped working. via says how: pause (active key, remaining time kept), ban (unused key) or local (license server had no record).

license.frozen payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "license.frozen",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "license_id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
    "key": "ASX-7KQM-P2VD-9XHT",
    "reason": "Chargeback opened",
    "via": "pause"
  }
}
license.unfrozenKey unfrozen

A freeze was reversed; the key works again.

license.unfrozen payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "license.unfrozen",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "license_id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
    "key": "ASX-7KQM-P2VD-9XHT"
  }
}
license.refundedKey refunded

Staff refunded this key: it was deleted on the license server and amount_cents went back to your balance. Sent once per key; the whole refund also arrives as a single order.refunded.

license.refunded payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "license.refunded",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "license_id": "lic_01m3pqdzd3vjeeanhqv0y3p8bc",
    "key": "ASX-7KQM-P2VD-9XHT",
    "order_id": "ord_01m3pqbssww1nqmgmmf2k0yheb",
    "refund_id": "ref_01m3pqf62e0z7k7s39f746dmvy",
    "amount_cents": 2600
  }
}
balance.creditedBalance credited

Money was added to your balance: an approved top-up (source: topup, with topup_id) or a staff adjustment (source: adjustment).

balance.credited payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "balance.credited",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "amount_cents": 50000,
    "balance_cents": 51300,
    "source": "topup",
    "topup_id": "top_01m3pqf62e0z7k7s39f746dmvy"
  }
}
custom_request.updatedCustom work updated

A custom work request changed status. quote_cents is present when the status is quoted.

custom_request.updated payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "custom_request.updated",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "request_id": "req_01m3pqjt2fdx7eanpfmkab9n9d",
    "status": "quoted",
    "quote_cents": 25000
  }
}
pingTest ping

Sent when you press Send test on an endpoint in the seller desk. Acknowledge it with any 2xx.

ping payload
{
  "id": "evt_01m3pqg50afj780nh8mkn0fw14",
  "type": "ping",
  "created": "2026-09-29T14:03:12.918Z",
  "data": {
    "message": "Hello from Certware"
  }
}

OpenAPI

Everything on this page is generated from the same definitions that validate live requests, and is also published as an OpenAPI 3.1 document — import it into Postman or Insomnia, or generate a typed client with your favourite OpenAPI generator. Webhook payloads are in its webhooks section.

Open

Questions or a missing endpoint? Talk to us and quote the request id.