Menu
DevelopersAPI reference

API reference

Publish releases from CI with an organization API key, connect your own key server, and call the marketplace and license routes the site and the launcher use.

Base URL /api/v1Sample values only

Overview

Every route below is served under https://api.aimjunkies.net/api/v1. Authentication is an organization API key, a member session or a Bearer secret, depending on the route. All keys and tokens in the examples are samples.

Keep keys server-side. Never put a key or secret in client-facing code. CI scripts should use an organization API key (aimjunkies_live_...) with only the scopes they need. The gateway secret authenticates AimJunkies to your server, never the other way round.

Delivery gateway

WebSocket events

WSSwss://api.yourdomain.com/v1/aimjunkies-streamGateway secret (AUTH frame)

The endpoint you register as a wss:// Master Gateway. The Test button in the Developer Console connects to it, sends the AUTH frame below and checks your reply. Orders and key-stock pulls are delivered as authenticated HTTPS requests (next entry), so keep an HTTPS endpoint available as well.

Parameters for WebSocket events
NameInTypeDescription
actionrequiredframestringSet to "AUTH" for connection authentication, or "PING" for round-trip latency keepalive.
tokenrequiredframestringYour gateway secret (aj_master_sec_sample_99a8b7c6d5e4f3a2b1c0).
timestamprequiredframeintegerEpoch millisecond timestamp used for anti-replay verification.
Request, TypeScript
import { WebSocketServer, WebSocket } from 'ws';

const wss = new WebSocketServer({ port: 8081 });
const MASTER_TOKEN = process.env.AIMJUNKIES_MASTER_TOKEN || 'aj_master_sec_sample_99a8b7c6d5e4f3a2b1c0';

wss.on('connection', (ws: WebSocket) => {
  let authenticated = false;

  ws.on('message', (raw: string) => {
    const packet = JSON.parse(raw.toString());

    // 1. Verify Mutual Handshake
    if (packet.action === 'AUTH') {
      if (packet.token === MASTER_TOKEN) {
        authenticated = true;
        return ws.send(JSON.stringify({ status: 'AUTHENTICATED', orgId: 'org_developer_hub' }));
      }
      return ws.close(4001, 'Unauthorized Master Token');
    }

    if (!authenticated) return ws.close(4002, 'Unauthenticated stream');

    // 2. Handle Push Events
    if (packet.event === 'ORDER_FULFILLMENT') {
      ws.send(JSON.stringify({
        status: 'SUCCESS',
        orderId: packet.orderId,
        licenseKey: 'SAMPLE-KEY-99A1-B882-C773',
        instructions: 'Run loader as Admin.'
      }));
    }
  });
});
Response 101
{
  "status": "AUTHENTICATED",
  "orgId": "org_developer_hub",
  "operationalStatus": "OPERATIONAL",
  "latencyMs": 12
}

HTTPS fulfillment

POSThttps://api.yourdomain.com/v1/aimjunkies-gatewayAuthorization: Bearer <your secret>

The endpoint you register for your own key server. AimJunkies POSTs here when an order is paid (ORDER_FULFILLMENT), to pull key stock (GENERATE_KEY_STOCK) and when you press Test (INTEGRATION_TEST). Every request carries your secret as a Bearer token and a deterministic X-Idempotency-Key: answer a repeated key with the result you gave the first time. Only https:// endpoints are accepted and redirects are not followed.

Parameters for HTTPS fulfillment
NameInTypeDescription
AuthorizationrequiredheaderstringBearer <your secret>. Compare it in constant time and refuse anything else.
X-Idempotency-KeyrequiredheaderstringStable per fulfillment (the order item id, or the stock batch). A retry repeats it; return the same result, never a second license.
eventrequiredbodystringEvent topic: "ORDER_FULFILLMENT", "GENERATE_KEY_STOCK", or "INTEGRATION_TEST".
Request, TypeScript
import express, { Request, Response } from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());
const MASTER_TOKEN = process.env.AIMJUNKIES_MASTER_TOKEN || 'aj_master_sec_sample_99a8b7c6d5e4f3a2b1c0';

function verifySecret(req: Request, res: Response, next: Function) {
  // Constant-time comparison of "Bearer <your secret>"
  const trusted = Buffer.from('Bearer ' + MASTER_TOKEN, 'utf8');
  const untrusted = Buffer.from((req.headers['authorization'] as string) || '', 'utf8');
  if (trusted.length !== untrusted.length || !crypto.timingSafeEqual(trusted, untrusted)) {
    return res.status(401).json({ status: 'FAILED', message: 'Invalid secret' });
  }
  next();
}

app.post('/v1/aimjunkies-gateway', verifySecret, (req, res) => {
  res.json({ status: 'SUCCESS', operationalStatus: 'OPERATIONAL' });
});
Response 200
{
  "status": "SUCCESS",
  "operationalStatus": "OPERATIONAL",
  "latencyMs": 14
}

Releases and builds

Upload a build

POST/api/v1/organizations/:orgId/assetsAPI key (X-API-Key) or member session

Registers a file in your organization’s Build Vault and returns a short-lived upload URL. PUT the file to that URL, then call POST …/assets/:assetId/versions/:versionId/finalize to verify it. The version id you get back is the assetVersionId a release points at. New versions of the same asset use POST …/assets/:assetId/versions.

Parameters for Upload a build
NameInTypeDescription
X-API-KeyrequiredheaderstringOrganization API key (aimjunkies_live_sample_8a9b0c1d2e3f4a5b6c7d8e9f). Roles OWNER, ADMIN or BUILDER; a key can also be sent as Authorization: Bearer.
namerequiredbodystringAsset name, 2 to 100 characters.
filenamerequiredbodystringFile name (letters, digits, dot, dash, underscore).
mimeTyperequiredbodystringMIME type of the file.
sizeBytesrequiredbodyintegerFile size in bytes, up to 5 GB.
versionrequiredbodystringSemantic version, for example 1.0.0 or 1.0.0-rc1.
checksumSha256bodystringSHA-256 of the file, checked at finalize.
changelogbodystringNotes for this version.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/organizations/org_developer_hub/assets', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.AJ_API_KEY!, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Apex loader',
    filename: 'loader-4.2.8.exe',
    mimeType: 'application/octet-stream',
    sizeBytes: 8421376,
    version: '4.2.8',
  }),
});
const { asset, uploadUrl } = await res.json();
Response 201
{
  "asset": {
    "id": "ast_…",
    "name": "Apex loader",
    "versions": [
      {
        "id": "ver_…",
        "version": "4.2.8"
      }
    ]
  },
  "uploadUrl": "https://…",
  "expiresInSeconds": 900,
  "expiresAt": "2026-11-10T12:15:00.000Z"
}

List releases

GET/api/v1/organizations/:orgId/catalog/products/:productId/releasesAPI key (X-API-Key) or member session

Every release of one of your products, newest first, with its status (TESTING, LIVE, SUPERSEDED, ROLLED_BACK) and live detection status.

Parameters for List releases
NameInTypeDescription
X-API-KeyrequiredheaderstringOrganization API key (aimjunkies_live_sample_8a9b0c1d2e3f4a5b6c7d8e9f).
orgIdrequiredpathstringYour organization id.
productIdrequiredpathstringThe product id.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/organizations/org_developer_hub/catalog/products/PRODUCT_ID/releases', {
  headers: { 'X-API-Key': process.env.AJ_API_KEY! },
});
const releases = await res.json();
Response 200
[
  {
    "id": "rel_…",
    "version": "4.2.8",
    "gameBuild": "1.2.3",
    "status": "LIVE",
    "detectionStatus": "UNDETECTED",
    "promotedAt": "2026-11-09T18:00:00.000Z"
  }
]

Create a release

POST/api/v1/organizations/:orgId/catalog/products/:productId/releasesAPI key (X-API-Key) or member session

Creates a release from a finalized Build Vault version. It starts in TESTING; buyers keep getting the live release until you promote this one. Detection status is never set with an API key.

Parameters for Create a release
NameInTypeDescription
X-API-KeyrequiredheaderstringOrganization API key (aimjunkies_live_sample_8a9b0c1d2e3f4a5b6c7d8e9f).
versionrequiredbodystringRelease version: letters, digits and . _ + - only, up to 40 characters.
gameBuildbodystringThe game build it was verified on.
notesbodystringRelease notes, up to 5000 characters.
assetVersionIdbodystringThe Build Vault asset version this release ships.
cliArgumentPatternbodystringCommand line the launcher passes a console loader; {KEY} becomes the licence key.
Request, TypeScript
await fetch('https://api.aimjunkies.net/api/v1/organizations/org_developer_hub/catalog/products/PRODUCT_ID/releases', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.AJ_API_KEY!, 'Content-Type': 'application/json' },
  body: JSON.stringify({ version: '4.2.8', gameBuild: '1.2.3', notes: 'Updated offsets.', assetVersionId: 'VAULT_VERSION_ID' }),
});
Response 201
{
  "id": "rel_…",
  "version": "4.2.8",
  "gameBuild": "1.2.3",
  "status": "TESTING",
  "assetVersionId": "ver_…",
  "createdAt": "2026-11-10T12:00:00.000Z"
}

Promote a release

POST/api/v1/organizations/:orgId/catalog/products/:productId/releases/:releaseId/promoteAPI key (X-API-Key) or member session

Makes the release the live one: buyers get it at once and the previous live release is superseded. An optional body opens a Prime early-access window (the release is live for Prime members first). POST …/releases/:releaseId/rollback goes back to the previous release.

Parameters for Promote a release
NameInTypeDescription
X-API-KeyrequiredheaderstringOrganization API key (aimjunkies_live_sample_8a9b0c1d2e3f4a5b6c7d8e9f).
primeEarlyAccessHoursbodyintegerHours the release is live for Prime members before everyone else. Capped by the admin limit.
Request, TypeScript
await fetch('https://api.aimjunkies.net/api/v1/organizations/org_developer_hub/catalog/products/PRODUCT_ID/releases/RELEASE_ID/promote', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.AJ_API_KEY! },
});
Response 200
{
  "id": "rel_…",
  "version": "4.2.8",
  "status": "LIVE",
  "promotedAt": "2026-11-10T12:05:00.000Z"
}

Webhooks

Register a webhook

POST/api/v1/organizations/:orgId/webhooksAPI key (X-API-Key) or member session

Registers an HTTPS endpoint for your organization (public addresses only, no redirects). AimJunkies POSTs a JSON event to it when an order line of yours completes (order.completed), when a license is issued to a buyer of your product (license.issued) and when a line is refunded in full or in part (refund.processed); all three when events is left out. The body carries ids and statuses only, never a key, a secret field or an email: {"id", "type", "createdAt", "organizationId", "data": {"orderId", "orderItemId", "offerId", "productId", "status"}}; read the rest with your API key. Each request has the headers X-AimJunkies-Event, X-AimJunkies-Delivery (the same id on every retry: drop repeats) and X-AimJunkies-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with the endpoint secret. Answer with any 2xx within 10 seconds; anything else is retried (6 attempts, 30 seconds then doubling) and then marked FAILED. The secret is returned once, here. GET …/webhooks lists endpoints without secrets, DELETE …/webhooks/:webhookId removes one (scope webhooks:write), and GET …/webhooks/deliveries shows every attempt with status code, response excerpt and duration (webhooks:read).

Parameters for Register a webhook
NameInTypeDescription
X-API-KeyrequiredheaderstringOrganization API key (aimjunkies_live_sample_8a9b0c1d2e3f4a5b6c7d8e9f).
urlrequiredbodystringYour https:// endpoint.
eventsbodystring[]order.completed, license.issued, refund.processed. Defaults to all three.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/organizations/org_developer_hub/webhooks', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.AJ_API_KEY!, 'Content-Type': 'application/json' },
  body: JSON.stringify({ url: 'https://hooks.example.com/aimjunkies', events: ['order.completed'] }),
});
const { id, secret } = await res.json(); // keep the secret: it is shown once
Response 201
{
  "id": "whk_…",
  "organizationId": "org_developer_hub",
  "url": "https://hooks.example.com/aimjunkies",
  "secret": "whsec_…",
  "events": [
    "order.completed",
    "refund.processed"
  ],
  "status": "ACTIVE",
  "createdAt": "2026-11-10T12:00:00.000Z"
}

Licenses & HWID

Activate a license

POST/api/v1/licenses/activateNo auth

Binds a license key to a hardware ID on first use (or carries it to the same machine after a part swap) and returns a signed status valid for one hour. Rate limited to 10 requests a minute.

Parameters for Activate a license
NameInTypeDescription
keyrequiredbodystringThe license key.
hwidrequiredbodystringThe machine’s hardware ID.
hwidComponentsbodyobjectPer-component hashes, used to recognise the same machine after a part swap.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/licenses/activate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'AJ-APEX-9921-X99B-814A', hwid: 'c7a93b4e182f09d6e84a2c5b3d1f8e7a' }),
});
const status = await res.json();
Response 200
{
  "licenseId": "lic_…",
  "hwid": "c7a93b4e182f09d6e84a2c5b3d1f8e7a",
  "status": "ACTIVE",
  "expiresAt": "2026-11-10T13:00:00.000Z",
  "signature": "…"
}

Validate a license

POST/api/v1/licenses/validateNo auth

Checks that a key is still valid for a hardware ID and returns a fresh signed status. Clients re-check about once an hour; a suspended or revoked license stops validating within that hour. Rate limited to 120 requests a minute (20 per key).

Parameters for Validate a license
NameInTypeDescription
keyrequiredbodystringThe license key.
hwidrequiredbodystringThe machine’s hardware ID.
hwidComponentsbodyobjectPer-component hashes.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/licenses/validate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'AJ-APEX-9921-X99B-814A', hwid: 'c7a93b4e182f09d6e84a2c5b3d1f8e7a' }),
});
const status = await res.json();
Response 200
{
  "licenseId": "lic_…",
  "hwid": "c7a93b4e182f09d6e84a2c5b3d1f8e7a",
  "status": "ACTIVE",
  "expiresAt": "2026-11-10T13:00:00.000Z",
  "signature": "…"
}

Reset your own HWID

POST/api/v1/licenses/:id/hwid-reset/selfMember session (Authorization: Bearer)

The license owner resets the hardware binding within the allowance of their plan (GET /api/v1/licenses/:id/hwid-reset/status shows how many are left and when the next one is due). Needs a signed-in member session; five requests an hour.

Parameters for Reset your own HWID
NameInTypeDescription
idrequiredpathstringThe license id (from GET /api/v1/licenses/my).
AuthorizationrequiredheaderstringBearer access token of the license owner.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/licenses/LICENSE_ID/hwid-reset/self', {
  method: 'POST',
  headers: { Authorization: `Bearer ${accessToken}` },
});
const allowance = await res.json(); // { allowed, remaining, nextAt, … }
Response 200
{
  "allowed": true,
  "blocked": null,
  "limit": 3,
  "used": 1,
  "remaining": 2,
  "periodDays": 365,
  "lifetime": false,
  "nextAt": null
}

Marketplace

Search offers

GET/api/v1/catalog/market/offersNo auth

Public, paged search over every live offer. Filter by section, game, offer type, product or seller, search with q, and sort. Offer-type attributes (for example region on a Steam key) can be sent as extra query parameters; GET /api/v1/catalog/market/facets lists the filters that apply. GET /api/v1/catalog/market/browse returns the section and game tree, and GET /api/v1/catalog/market/listings/:slug one listing with its offers.

Parameters for Search offers
NameInTypeDescription
sectionquerystringSection slug, for example cheats or accounts.
gamequerystringGame slug.
typequerystringOffer type slug, for example steam-key.
productquerystringProduct slug.
sellerquerystringSeller organization slug.
qquerystringText search.
sortquerystringSort order.
minPrice / maxPricequerynumberPrice range, in US dollars.
page / limitqueryintegerPaging.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/catalog/market/offers?section=keys-cards&type=steam-key&limit=20');
const offers = await res.json();
Response 200
{
  "offers": [
    {
      "id": "off_…",
      "title": "…",
      "listing": {
        "slug": "…"
      },
      "seller": {
        "name": "…"
      },
      "purchasable": true
    }
  ],
  "filterable": [],
  "meta": {
    "total": 1,
    "page": 1,
    "limit": 20,
    "pages": 1,
    "sort": "relevance"
  }
}

Quote a cart

POST/api/v1/catalog/market/quoteNo auth

Prices a cart without buying anything: each line is checked against its offer (stock, plan, answers to the offer type’s checkout questions) and comes back priced or flagged. POST /api/v1/orders/quote does the same for a signed-in buyer, with their Prime and coupon discounts.

Parameters for Quote a cart
NameInTypeDescription
linesrequiredbodyarrayOne entry per offer: offerId, and where the offer needs them planDays (days or "lifetime"), units, quantity and buyerInputs.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/catalog/market/quote', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ lines: [{ offerId: 'OFFER_ID', planDays: 30, quantity: 1 }] }),
});
const quote = await res.json();
Response 200
{
  "lines": [
    {
      "index": 0,
      "offerId": "OFFER_ID",
      "status": "ok",
      "issues": [],
      "deliveryEstimate": "Instant"
    }
  ],
  "subtotalCents": 0,
  "currency": "USD",
  "ready": true,
  "checkout": {
    "available": true,
    "code": null
  }
}

Orders

Place an order

POST/api/v1/ordersMember session (Authorization: Bearer)

Creates an order from offers for the signed-in member; each item becomes its own order line with its own status, delivery and protection window. The checkout page adds a bot check (Turnstile), so this route is meant for the site and apps, not for unattended scripts. Pay the order from the Wallet or through the payment provider checkout; the money is held in escrow until each line completes.

Parameters for Place an order
NameInTypeDescription
itemsrequiredbodyarray1 to 100 items: offerId, and as the offer needs planDays, units, quantity, buyerInputs, seenLineTotalCents.
couponCodebodystringA coupon code.
giftbodyobjectrecipient and message, to send the order as a gift.
Request, TypeScript
const res = await fetch('https://api.aimjunkies.net/api/v1/orders', {
  method: 'POST',
  headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ items: [{ offerId: 'OFFER_ID', planDays: 30, quantity: 1 }] }),
});
const order = await res.json();
Response 201
{
  "id": "ord_…",
  "status": "PENDING",
  "items": [
    {
      "id": "itm_…",
      "lineStatus": "AWAITING_PAYMENT"
    }
  ]
}

Confirm receipt

POST/api/v1/orders/lines/:id/confirmMember session (Authorization: Bearer)

The buyer confirms an order line is received (status DELIVERED to RECEIVED). Without it the line confirms by itself after the offer type’s auto-confirm time; either way the protection window starts then. GET /api/v1/orders/lines/my lists your lines and GET /api/v1/orders/lines/:id shows one with its timers.

Parameters for Confirm receipt
NameInTypeDescription
idrequiredpathstringThe order line id.
Request, TypeScript
await fetch('https://api.aimjunkies.net/api/v1/orders/lines/LINE_ID/confirm', {
  method: 'POST',
  headers: { Authorization: `Bearer ${accessToken}` },
});
Response 200
{
  "id": "line_…",
  "status": "PROTECTION",
  "timers": {
    "autoConfirmAt": null
  }
}

Open a dispute

POST/api/v1/orders/lines/:id/disputeMember session (Authorization: Bearer)

The buyer disputes an order line from delivery until its protection window ends. Every timer on the line stops and the money stays held; the seller answers in the order chat (POST …/dispute/respond), and staff decide a full refund, a partial refund or a release. POST …/dispute/withdraw takes it back.

Parameters for Open a dispute
NameInTypeDescription
idrequiredpathstringThe order line id.
reasonrequiredbodystringWhat is wrong.
requestedrequiredbodystringfull_refund, partial_refund or replacement.
amountCentsbodyintegerFor a partial refund: the amount asked for.
evidenceAttachmentIdsbodystring[]Up to 5 attachments from the order chat.
Request, TypeScript
await fetch('https://api.aimjunkies.net/api/v1/orders/lines/LINE_ID/dispute', {
  method: 'POST',
  headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ reason: 'The key was already redeemed.', requested: 'full_refund' }),
});
Response 201
{
  "id": "line_…",
  "status": "DISPUTED"
}
Ready to connect? Create API keys with the scopes your pipeline needs, connect and test your gateway, and rotate secrets in the Developer Console
Did this answer your question?