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.
Quick start
- Create a key in the seller desk under Developers. Give it only the scopes your integration needs; the secret is shown once.
- Check it works with
GET /me, then pick avariant_idfromGET /products. - When a customer pays, call
POST /orderswith anIdempotency-Key. Hand themlicenses[].keyfrom the response. - Optionally add a webhook endpoint in the same Developers page to hear about
order.fulfilled, key redemptions, freezes and balance top-ups.
curl "https://certware.xyz/api/v1/me" \
-H "Authorization: Bearer $CERTWARE_API_KEY"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_casekeys. SendContent-Type: application/jsonwith request bodies; unknown body fields are rejected so typos don't pass silently. - Money is always an integer number of US cents:
1300is $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. Uselimitto size a page (andoffseton/licenses). - Every response carries an
X-Request-Idheader (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}
- licenses:write
- Reset HWIDs, freeze and unfreeze keys.POST /licenses/{key_or_id}/reset-hwidPOST /licenses/{key_or_id}/freezePOST /licenses/{key_or_id}/unfreeze
- 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 —
200withIdempotent-Replayed: true, the same keys, no second charge. Safe to retry after any timeout, network error,429or500. - First request still running
409 idempotency_key_in_usewithRetry-Afterand 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 isfailedand fully refunded, and the key stays bound to it. Place the order again with a new key.- Rejected before charging
400,402,403and 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.
{
"error": {
"code": "insufficient_funds",
"message": "Not enough balance. Top up first.",
"details": {
"available_cents": 1000,
"required_cents": 2600
},
"request_id": "req_01m3pqjt2fdx7eanpfmkab9n9d"
}
}- invalid_requestHTTP 400
- Malformed JSON, a field failed validation (
paramnames it), or a required header is missing. Fix the request. - invalidHTTP 400
- The request broke a business rule, e.g. a variant that's no longer sold. Fix the request.
- unauthenticatedHTTP 401
- The key is missing, invalid, expired or revoked, or its shop isn't active. Check the key.
- insufficient_fundsHTTP 402
- Your balance can't cover the order.
detailshasavailable_centsandrequired_cents. Nothing was charged. Top up, then retry with the same Idempotency-Key. - insufficient_scopeHTTP 403
- The key lacks the endpoint's scope (
details.required_scope). Use a key with that scope. - forbiddenHTTP 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_foundHTTP 404
- No such resource, or it belongs to another shop. Don't retry.
- conflictHTTP 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_useHTTP 409
- The first request with this Idempotency-Key is still being fulfilled. The body includes
order. Retry with the same key afterRetry-Afterseconds. - payload_too_largeHTTP 413
- Request bodies are limited to 64 KB. Don't retry.
- rate_limitedHTTP 429
- Too many requests. Retry after
Retry-Afterseconds. - internal_errorHTTP 500
- Our bug. It was logged with the request id. Retry with backoff (orders: same Idempotency-Key).
- fulfillment_failedHTTP 502
- The license server failed to mint. The order is
failed, refunded in full, and included asorder. Retry with a new Idempotency-Key. - unavailableHTTP 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 / minuteper API key
- Every request.
- 60 / minuteper shop
POST /orders, across all of the shop's keys (every order mints keys on the license server).- 120 / hourper shop
- HWID resets, freezes and unfreezes — shared with the same actions in the seller desk.
Account
The shop and key behind a request.
/api/v1/meGet 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.
curl "https://certware.xyz/api/v1/me" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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"
}
}Catalog
What you can sell, at your partner price.
/api/v1/productsList 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.
curl "https://certware.xyz/api/v1/products" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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
}Orders
Buy keys from your balance; they're minted and returned instantly.
/api/v1/ordersCreate 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.
Parameters
- Idempotency-Keyheaderstringrequired
- 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.
- linesbodyarray of objectrequired
- What to buy: 1–20 lines, at most 200 keys in total.
- lines[].variant_idbodystringrequired
- Variant to buy (var_…), from GET /products.
- lines[].quantitybodyintegerrequired
- Keys of this variant, 1–100.
- external_refbodystringoptional
- Your own reference, e.g. your storefront invoice id. Echoed back and included in webhooks.
- customer_refbodystringoptional
- Your customer's email or Discord, for your records.
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"
}'{
"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
/api/v1/ordersList 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.
Parameters
- statusquery"pending" | "fulfilling" | "fulfilled" | "failed" | "refunded" | "partially_refunded"optional
- Only orders in this status.
- external_refquerystringoptional
- Only the order(s) you created with this external_ref.
- starting_afterquerystringoptional
- Cursor: the id of the last order on the previous page.
- limitqueryintegeroptional
- Page size, 1–100. Default 25.
curl "https://certware.xyz/api/v1/orders?limit=25" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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
}/api/v1/orders/{id}Get an order
One of your orders with its lines and delivered keys. Other shops' orders are always 404.
Parameters
- idpathstringrequired
- —
curl "https://certware.xyz/api/v1/orders/ord_01m3pqdfy5zdyem2682rr5n1ef" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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"
}
]
}Licenses
Look up the keys you've sold and manage them for your customers.
/api/v1/licensesList 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.
Parameters
- product_idquerystringoptional
- Only keys for this product.
- statusquery"active" | "frozen" | "refunded" | "revoked"optional
- Only keys in this status.
- claimedquery"true" | "false"optional
true: only keys a customer has redeemed.false: only unredeemed keys.- qquerystringoptional
- Search: part of a key, an order id, part of a Discord username, or an exact Discord id.
- limitqueryintegeroptional
- Page size, 1–100. Default 50.
- offsetqueryintegeroptional
- Items to skip. Default 0.
curl "https://certware.xyz/api/v1/licenses?claimed=true&limit=50" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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
}/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.
Parameters
- key_or_idpathstringrequired
- A license id (lic_…) or the key itself (case-insensitive).
curl "https://certware.xyz/api/v1/licenses/ASX-7KQM-P2VD-9XHT" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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"
}
]
}/api/v1/licenses/{key_or_id}/reset-hwidReset 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.
Parameters
- key_or_idpathstringrequired
- A license id (lic_…) or the key itself (case-insensitive).
curl -X POST "https://certware.xyz/api/v1/licenses/ASX-7KQM-P2VD-9XHT/reset-hwid" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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"
}/api/v1/licenses/{key_or_id}/freezeFreeze 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.
Parameters
- key_or_idpathstringrequired
- A license id (lic_…) or the key itself (case-insensitive).
- reasonbodystringrequired
- Why the key is frozen. Stored on the key and sent in the license.frozen webhook.
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"
}'{
"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"
}/api/v1/licenses/{key_or_id}/unfreezeUnfreeze 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.
Parameters
- key_or_idpathstringrequired
- A license id (lic_…) or the key itself (case-insensitive).
curl -X POST "https://certware.xyz/api/v1/licenses/ASX-7KQM-P2VD-9XHT/unfreeze" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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"
}Balance
Your prepaid balance and its ledger.
/api/v1/balanceGet your balance
Your spendable balance. Top up from the seller desk (Wallet); approved top-ups send balance.credited.
curl "https://certware.xyz/api/v1/balance" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"object": "balance",
"available_cents": 48700,
"currency": "USD"
}/api/v1/ledgerList 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.
Parameters
- kindquery"topup" | "order" | "refund" | "tier_upgrade" | "referral_credit" | "custom_work" | "custom_work_refund" | "adjustment"optional
- Only entries of this kind.
- limitqueryintegeroptional
- Page size, 1–100. Default 50.
curl "https://certware.xyz/api/v1/ledger?limit=2" \
-H "Authorization: Bearer $CERTWARE_API_KEY"{
"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
}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.
/api/v1/admin/shopsList shops
Shops newest first, with tier and balance. Filter status=pending for the application queue.
Parameters
- statusquery"pending" | "active" | "rejected" | "suspended"optional
- Only shops in this status.
- qquerystringoptional
- Search name, slug or contact email (partial), or an exact shop id.
- limitqueryintegeroptional
- Page size, 1–100. Default 50.
curl "https://certware.xyz/api/v1/admin/shops?status=active" \
-H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"{
"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
}/api/v1/admin/shops/{id}/approveApprove 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.
Parameters
- idpathstringrequired
- —
- tier_idbodystringoptional
- Tier to start the shop on. Defaults to the default tier.
- notebodystringoptional
- Internal review note.
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."
}'{
"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"
}/api/v1/admin/shops/{id}/rejectReject a shop application
Declines a pending application and emails the applicant the reason.
Parameters
- idpathstringrequired
- —
- reasonbodystringrequired
- Shown to the applicant in the rejection email.
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."
}'{
"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"
}/api/v1/admin/topupsList top-up requests
Balance top-ups filed by shops. With status=pending the queue comes oldest first (review order); otherwise newest first.
Parameters
- statusquery"pending" | "approved" | "rejected" | "cancelled" | "refunded" | "disputed"optional
- Only top-ups in this status.
- shop_idquerystringoptional
- Only this shop's top-ups.
- limitqueryintegeroptional
- Page size, 1–100. Default 50.
curl "https://certware.xyz/api/v1/admin/topups?status=pending" \
-H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"{
"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
}/api/v1/admin/topups/{id}/approveApprove 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.
Parameters
- idpathstringrequired
- —
- notebodystringoptional
- Optional note, kept on the top-up.
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."
}'{
"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"
}/api/v1/admin/topups/{id}/rejectReject a top-up
Declines a pending top-up without moving money and tells the shop why.
Parameters
- idpathstringrequired
- —
- reasonbodystringrequired
- Shown to the shop in the rejection notice and email.
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."
}'{
"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"
}/api/v1/admin/productsList all products
The whole catalog in display order, including drafts, disabled products and retired variants.
curl "https://certware.xyz/api/v1/admin/products" \
-H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"{
"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
}/api/v1/admin/products/{id}/detectionSet a product's detection status
Updates the status resellers watch before selling and notifies every active shop's desk with your note.
Parameters
- idpathstringrequired
- —
- detectionbody"undetected" | "testing" | "updating" | "detected" | "unknown"required
- —
- notebodystringrequired
- What changed. Broadcast to every active shop's desk.
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."
}'{
"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"
}/api/v1/admin/ordersList orders across shops
Most recent orders platform-wide, newest first. Filter status=failed to watch fulfillment problems.
Parameters
- shop_idquerystringoptional
- Only this shop's orders.
- statusquery"pending" | "fulfilling" | "fulfilled" | "failed" | "refunded" | "partially_refunded"optional
- Only orders in this status.
- limitqueryintegeroptional
- Page size, 1–100. Default 50.
curl "https://certware.xyz/api/v1/admin/orders?status=failed" \
-H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"{
"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
}/api/v1/admin/licenses/{key_or_id}/freezeFreeze any key
Freezes a key regardless of the shop that owns it. Same behavior as the shop endpoint; the owning shop gets license.frozen.
Parameters
- key_or_idpathstringrequired
- A license id (lic_…) or the key itself (case-insensitive).
- reasonbodystringrequired
- Why the key is frozen. Stored on the key and sent in the license.frozen webhook.
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"
}'{
"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"
}/api/v1/admin/licenses/{key_or_id}/unfreezeUnfreeze any key
Reverses a freeze on any shop's key. The owning shop gets license.unfrozen.
Parameters
- key_or_idpathstringrequired
- A license id (lic_…) or the key itself (case-insensitive).
curl -X POST "https://certware.xyz/api/v1/admin/licenses/ASX-7KQM-P2VD-9XHT/unfreeze" \
-H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"{
"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"
}/api/v1/admin/licenses/{key_or_id}/reset-hwidReset any key's HWID
Unbinds any shop's key from its machine. The owning shop gets license.hwid_reset.
Parameters
- key_or_idpathstringrequired
- A license id (lic_…) or the key itself (case-insensitive).
curl -X POST "https://certware.xyz/api/v1/admin/licenses/ASX-7KQM-P2VD-9XHT/reset-hwid" \
-H "Authorization: Bearer $CERTWARE_PLATFORM_KEY"{
"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"
}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.
{
"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
idin 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.
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);
}
// 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
2xxwithin 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 inCertware-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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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).
{
"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.
{
"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.
{
"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).
{
"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.
{
"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.
{
"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.
Questions or a missing endpoint? Talk to us and quote the request id.