RMT.GG/Seller developer docs
v1

Seller API

Automate listings, restock keys and accounts, fulfill sales, and stream order events. Includes outbound webhooks and reserve endpoints for on-demand inventory after payment.

REST Open API

Bearer-authenticated /api/v1 for offers, stock, and orders, with discovery and rate-limit headers.

Outbound webhooks

Signed HTTPS (or Discord) deliveries for order and offer lifecycle events.

On-demand inventory

Mint COMPLEX stock from your HTTPS API after payment when local inventory is short.

What you can build

The Seller Open API is for sellers who want Discord alerts, stock sync, Zapier-style automation, or a custom back office on top of RMT.GG.

  • Manage offers
    Create drafts, update safe fields, publish, archive, and restock via /api/v1.
  • Fulfill sales
    List and inspect seller orders, then mark them delivered with optional evidence URLs.
  • Stay under the limit
    Every key is capped at 300 requests per minute. Responses include X-RateLimit-* headers.
  • React in real time
    Subscribe to order and offer events, or refill COMPLEX inventory with an on-demand endpoint.
  • Take payments from your shop
    Approved partners can send buyers from an external shop to hosted checkout, then fulfill on checkout.completed.

Quick start

Create an API key in Developer settings, then call discovery to print the live catalog.

  1. 1Open Settings → Developer (no separate enable step).
  2. 2Create an API key and copy the secret once (rmt_sk_live_…). Store it in your secret manager.
  3. 3Call GET /api/v1 with Authorization: Bearer to confirm scopes, quotas, and operations.
GET/api/v1

Discovery document

Returns scopes, quotas, webhook events, and the full operations catalog, including stock. Any valid API key works.

Authentication

Send your live secret key on every /api/v1 request. Prefer HTTPS only. Never embed keys in public clients or browser bundles.

Preferred header

http
Authorization: Bearer rmt_sk_live_<prefix>_<secret>

Alternate header

http
X-Api-Key: rmt_sk_live_<prefix>_<secret>

Reusable TypeScript client (Bearer auth, typed errors, 429 retry)

typescript
const API_BASE = "https://rmt.gg/api/v1";
const API_KEY = process.env.RMT_API_KEY!; // rmt_sk_live_…

export class RmtApiError extends Error {
  constructor(
    readonly status: number,
    readonly code: string | undefined,
    message: string,
    readonly retryAfterSec?: number,
  ) {
    super(message);
    this.name = "RmtApiError";
  }
}

type RmtFetchInit = RequestInit & { idempotencyKey?: string };

export async function rmtFetch<T>(path: string, init: RmtFetchInit = {}): Promise<T> {
  const headers = new Headers(init.headers);
  headers.set("Authorization", `Bearer ${API_KEY}`);
  // Alternate: headers.set("X-Api-Key", API_KEY);
  headers.set("Accept", "application/json");
  if (init.body && !headers.has("Content-Type")) {
    headers.set("Content-Type", "application/json");
  }
  if (init.idempotencyKey) headers.set("Idempotency-Key", init.idempotencyKey);

  const res = await fetch(`${API_BASE}${path}`, { ...init, headers });
  const retryAfter = Number(res.headers.get("Retry-After") ?? "");
  const body = (await res.json().catch(() => ({}))) as {
    error?: string;
    code?: string;
    retryAfter?: number;
  };

  if (res.status === 429) {
    throw new RmtApiError(
      429,
      body.code ?? "RATE_LIMITED",
      body.error ?? "Rate limited",
      Number.isFinite(retryAfter) ? retryAfter : body.retryAfter,
    );
  }
  if (!res.ok) {
    throw new RmtApiError(res.status, body.code, body.error ?? res.statusText);
  }
  return body as T;
}

export async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 4): Promise<T> {
  let attempt = 0;
  for (;;) {
    try {
      return await fn();
    } catch (err) {
      attempt += 1;
      if (!(err instanceof RmtApiError) || err.status !== 429 || attempt >= maxAttempts) {
        throw err;
      }
      const waitSec = Math.max(1, err.retryAfterSec ?? 1);
      await new Promise((r) => setTimeout(r, waitSec * 1000));
    }
  }
}

Rotate on leak

If a key leaks, revoke it in Developer settings and create a new one. Update your automation before revoking if you are live.

Scopes

Each API key carries scopes that gate endpoints. Missing scope returns 403 SCOPE_MISSING.

offers:read
offers:write
orders:read
orders:write
webhooks:manage
checkout:write
  • offers:read: List and get your offers, plus stock counts and delivery field names.
  • offers:write: Create, update, publish, and delete offers. Restock quantity and saved items.
  • orders:read: List and get seller orders.
  • orders:write: Mark orders delivered.
  • webhooks:manage: Reserved for future Open API webhook management. Configure Discord/Telegram in Notifications and JSON webhooks in Hosted checkout or Developer settings today.
  • checkout:write: Create and read hosted checkout sessions. Requires admin-approved partner checkout.

Default key scopes

New keys on Developer receive offers:read, offers:write, orders:read, orders:write, and checkout:write. Hosted checkout keys from that settings page get checkout:write and orders:read. Outbound webhook CRUD stays in the Settings UI (session auth).

Offers API

Offer identifiers accept the public url slug or numeric id. Responses omit internal id and sellerId.

What PATCH cannot change yet

Option prices, media, and attributes are still managed in the seller editor. Inventory uses GET /api/v1/stock and POST /api/v1/offers/:url/stock.

GET/api/v1/offers
offers:read

List your offers

Filter with archive=active (default), archived, or all.

Request

  • archive
    In
    query
    Type
    string
    Required
    Optional
    Description
    One of "active" (default), "archived", or "all".
  • Response: { offers: Offer[], total: number }. Numeric id and sellerId are omitted.
POST/api/v1/offers
offers:write

Create a draft offer

Creates an empty draft owned by the authenticated seller. No body required.

  • No request body required.
  • Response 201: { offer: Offer }.
GET/api/v1/offers/:urlOrId
offers:read

Get one offer

Load by public url slug or numeric id. Relations (options) may be included; stock items are not.

Request

  • urlOrId
    In
    path
    Type
    string
    Required
    Required
    Description
    Offer.url slug or Offer.id.
  • Returns relations (options, etc.) when available; items are not included.
PATCH/api/v1/offers/:urlOrId
offers:write

Update offer fields

Patch a safe subset of listing fields. Emits offer.updated when outbound webhooks are configured.

Request

  • urlOrId
    In
    path
    Type
    string
    Required
    Required
    Description
    Offer.url slug or Offer.id.
  • title
    In
    body
    Type
    string
    Required
    Optional
    Description
    Listing title.
  • description
    In
    body
    Type
    string
    Required
    Optional
    Description
    Listing description.
  • visibility
    In
    body
    Type
    string
    Required
    Optional
    Description
    PUBLIC | PRIVATE | UNPUBLISHED.
  • categoryId
    In
    body
    Type
    number
    Required
    Optional
    Description
    Catalog category id.
  • offeringId
    In
    body
    Type
    number
    Required
    Optional
    Description
    Catalog offering id.
  • thumbnail
    In
    body
    Type
    string
    Required
    Optional
    Description
    Thumbnail URL or asset reference.
  • offerType
    In
    body
    Type
    string
    Required
    Optional
    Description
    Offer type string used by the listing.
  • listingMode
    In
    body
    Type
    string
    Required
    Optional
    Description
    Listing mode (for example STANDARD, RANK_BOOST, SESSION).
  • At least one allowed field is required.
  • Emits offer.updated webhook when configured.
  • Stock is managed via GET/POST /api/v1/offers/:urlOrId/stock. Option prices, media, and attributes are not editable via this endpoint yet.
DELETE/api/v1/offers/:urlOrId
offers:write

Delete or archive

Same delete/archive rules as the seller UI.

Request

  • urlOrId
    In
    path
    Type
    string
    Required
    Required
    Description
    Offer.url slug or Offer.id.
  • Response: { ok: true }.
POST/api/v1/offers/:urlOrId/publish
offers:write

Publish an offer

Publishes a draft (or changes visibility). Fails with 400 if required listing fields are incomplete.

Request

  • urlOrId
    In
    path
    Type
    string
    Required
    Required
    Description
    Offer.url slug or Offer.id.
  • visibility
    In
    body
    Type
    string
    Required
    Optional
    Description
    Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED.
  • Response: { offer: Offer }.
  • Fails if the listing is incomplete for publish.

Stock API

See buyable counts per tier, match delivery field names to the right listing, then restock quantity or saved keys and accounts.

How matching works

GET /api/v1/stock?fields=username,password finds listings whose schema has those fields. Restock with option names (or optionId) and field names. You do not need internal field ids. Responses never include credential values.

GET/api/v1/stock
offers:read

List stock across your listings

Returns per-tier counts and delivery field names so you can match keys and accounts to the right offer. Filter with q, fields, stockMode, and lowStock. Never returns credential values.

Request

  • q
    In
    query
    Type
    string
    Required
    Optional
    Limits
    Max 80
    Description
    Filter by listing title or url slug.
  • fields
    In
    query
    Type
    string
    Required
    Optional
    Description
    Comma-separated delivery field names. The listing must have all of them (Username,Password). Names match case-insensitively.
  • stockMode
    In
    query
    Type
    string
    Required
    Optional
    Description
    QUANTITY or COMPLEX. Listing must have at least one option in that mode.
  • lowStock
    In
    query
    Type
    number
    Required
    Optional
    Description
    Keep listings that have a finite tier with available less than or equal to this number.
  • archive
    In
    query
    Type
    string
    Required
    Optional
    Description
    One of "active" (default), "archived", or "all".
  • Response: { offers: StockOffer[], total: number }. Numeric offer id is omitted. Option id is included so you can restock a specific tier.
  • available is the buyable count. null with unlimited true means unlimited quantity or on-demand COMPLEX inventory.
  • fields[] is the listing delivery schema (empty for quantity-only listings). Use it to map keys and accounts without field ids.
  • This endpoint never returns credential values.
GET/api/v1/offers/:urlOrId/stock
offers:read

Get stock for one listing

Same StockOffer shape as the index, for one url or numeric id. Counts only.

Request

  • urlOrId
    In
    path
    Type
    string
    Required
    Required
    Description
    Offer.url slug or Offer.id.
  • Response: { offer } with the same StockOffer shape as GET /api/v1/stock.
  • Counts only. Use the seller editor to inspect saved key values.
POST/api/v1/offers/:urlOrId/stock
offers:write

Restock a listing

Quantity tiers: add, remove, or set. Saved-item tiers: items objects by field name, keys[] when there is one field, or delimited text. Several tiers in one call via options[]. dryRun previews matching. onDuplicate defaults to skip.

json
{
  "option": "1 Month",
  "add": 50
}

Request

  • urlOrId
    In
    path
    Type
    string
    Required
    Required
    Description
    Offer.url slug or Offer.id.
  • option
    In
    body
    Type
    string
    Required
    Conditional
    Description
    Pricing option name (case-insensitive). Omit when the listing has a single tier.
  • optionId
    In
    body
    Type
    number
    Required
    Conditional
    Description
    Pricing option id from GET stock. Wins over option when both are sent. Ambiguous names return 409 OPTION_AMBIGUOUS.
  • add
    In
    body
    Type
    number
    Required
    Conditional
    Limits
    1-1,000,000
    Description
    QUANTITY: add this many units. Fails with 400 UNLIMITED_STOCK if the tier is unlimited.
  • remove
    In
    body
    Type
    number
    Required
    Conditional
    Limits
    1-1,000,000
    Description
    QUANTITY: withdraw this many units. Fails with 400 INSUFFICIENT_STOCK when there is not enough.
  • set
    In
    body
    Type
    number | null
    Required
    Conditional
    Description
    QUANTITY: set an absolute count. null means unlimited. Cannot go below units held in checkout.
  • items
    In
    body
    Type
    object[]
    Required
    Conditional
    Limits
    Max 1,000
    Description
    COMPLEX: objects keyed by delivery field name, for example { "Username": "a", "Password": "b" }. Names match case-insensitively.
  • keys
    In
    body
    Type
    string[]
    Required
    Conditional
    Limits
    Max 1,000
    Description
    COMPLEX: license keys when the listing has exactly one delivery field. Otherwise 400 FIELD_MAPPING_AMBIGUOUS.
  • text
    In
    body
    Type
    string
    Required
    Conditional
    Limits
    Max 1,000 rows
    Description
    COMPLEX: delimited paste. A header row that matches field names is detected automatically. Otherwise columns map in field sort order when the column count matches.
  • delimiter
    In
    body
    Type
    string
    Required
    Optional
    Limits
    Default :
    Description
    Delimiter for text. Ignored unless text is sent.
  • headers
    In
    body
    Type
    string[]
    Required
    Optional
    Description
    Optional column headers for text when the first line is data, not names.
  • options
    In
    body
    Type
    object[]
    Required
    Conditional
    Description
    Restock several tiers in one call. Each element is the same shape as a single-option body (option, add, items, …).
  • dryRun
    In
    body
    Type
    boolean
    Required
    Optional
    Description
    Preview matching and counts without writing. Default false.
  • onDuplicate
    In
    body
    Type
    string
    Required
    Optional
    Limits
    skip (default) or error
    Description
    COMPLEX: skip existing unsold fingerprints, or fail the request with 409 DUPLICATE_ITEMS.
  • Send exactly one action per option: add, remove, set, items, keys, or text.
  • Sending items to a QUANTITY tier (or add to a COMPLEX tier) returns 400 STOCK_MODE_MISMATCH.
  • Responses never echo credential values. COMPLEX results include imported, skippedDuplicates, errors, and matchedFields.
  • Saved items are capped at 5,000 unsold rows per option. A single request may import at most 1,000 rows.

Import many accounts or keys

Guide

POST /api/v1/offers/:url/stock with items[] for accounts or keys[] for single-field license codes. Chunk to 1,000 rows per request.

  1. 1GET the listing. Use fields[] and stockMode to choose items, keys, or add.
  2. 2Build items[] objects keyed by field name, or keys[] when the listing has one field. CSV or colon-separated rows can go in text.
  3. 3Dry-run first. Check wouldImport, skippedDuplicates, and matchedFields.
  4. 4POST the same body again without dryRun to write stock.

Pick the payload that matches the listing

Several delivery fields: items objects (Username, Password, E-Mail). Exactly one field: keys[]. Quantity listings: add, remove, or set. Never mix those actions on one option.

1,000 rows per request. 5,000 unsold items per tier. 300 requests per minute. Duplicates skip by default.

items[] JSON

json
[
  { "Username": "player1", "Password": "secret1", "E-Mail": "[email protected]" },
  { "Username": "player2", "Password": "secret2", "E-Mail": "[email protected]" }
]

CSV in the text field

csv
Username,Password,E-Mail
player1,secret1,p1@example.com
player2,secret2,p2@example.com

keys[] (one per line)

text
AAAA-BBBB-CCCC
DDDD-EEEE-FFFF
GGGG-HHHH-IIII

cURL

bash
# Inspect field names and stockMode
curl -s -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock

# Preview (no write)
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"dryRun":true,"onDuplicate":"skip","option":"Premium","items":[{"Username":"player1","Password":"secret1","E-Mail":"[email protected]"}]}'

# Apply accounts
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"onDuplicate":"skip","option":"Premium","items":[{"Username":"player1","Password":"secret1","E-Mail":"[email protected]"}]}'

# Apply license keys (listing must have exactly one delivery field)
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"option":"Steam","keys":["AAAA-BBBB-CCCC","DDDD-EEEE-FFFF"]}'

# Or paste CSV / colon-separated rows in text
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"option":"Premium","delimiter":",","text":"Username,Password,E-Mail\nplayer1,secret1,[email protected]"}'

TypeScript with rmtFetch from Auth (1,000 rows per request)

typescript
// Paste rmtFetch and withRetry from the Auth section first.
const CHUNK = 1000;

async function importRows(offerUrl: string, option: string, rows: Array<Record<string, string>>) {
  for (let i = 0; i < rows.length; i += CHUNK) {
    const items = rows.slice(i, i + CHUNK);
    const preview = await rmtFetch<{
      results: Array<{ wouldImport: number; skippedDuplicates: number; errors: string[] }>;
    }>(`/offers/${offerUrl}/stock`, {
      method: "POST",
      body: JSON.stringify({ dryRun: true, onDuplicate: "skip", option, items }),
    });
    const row = preview.results[0];
    if ((row?.errors?.length ?? 0) > 0) {
      throw new Error(row.errors.join("; "));
    }
    await withRetry(() =>
      rmtFetch(`/offers/${offerUrl}/stock`, {
        method: "POST",
        body: JSON.stringify({ onDuplicate: "skip", option, items }),
      }),
    );
  }
}

// License keys: only when GET stock.fields has exactly one name
async function importKeys(offerUrl: string, option: string, keys: string[]) {
  for (let i = 0; i < keys.length; i += CHUNK) {
    await withRetry(() =>
      rmtFetch(`/offers/${offerUrl}/stock`, {
        method: "POST",
        body: JSON.stringify({ option, keys: keys.slice(i, i + CHUNK) }),
      }),
    );
  }
}

Keep credentials off the wire logs

offers:write is required. You can only restock your own listings. GET and POST responses never echo saved values. Use HTTPS in production, store the key in an environment variable, and do not log the request body.

On Windows, use curl.exe (not the curl alias). Quote the -d JSON so PowerShell does not split it.

Orders API

Orders are scoped to your seller account. Buyer billing details may be redacted under marketplace-of-record privacy rules.

GET/api/v1/orders
orders:read

List seller orders

Supports limit, offset, status, q, and sort (newest, oldest, total_high, total_low).

Request

  • limit
    In
    query
    Type
    number
    Required
    Optional
    Limits
    1-100, default 20
    Description
    Page size.
  • offset
    In
    query
    Type
    number
    Required
    Optional
    Limits
    >= 0, default 0
    Description
    Skip this many rows.
  • status
    In
    query
    Type
    string
    Required
    Optional
    Limits
    Max 32
    Description
    Filter by order status (for example PAID, DELIVERED, COMPLETED).
  • q
    In
    query
    Type
    string
    Required
    Optional
    Limits
    Max 80
    Description
    Search reference or related text.
  • sort
    In
    query
    Type
    string
    Required
    Optional
    Limits
    newest (default)
    Description
    newest | oldest | total_high | total_low.
  • Response: { orders: Order[], total: number }.
GET/api/v1/orders/:uid
orders:read

Get one order

Returns the order with line items. Use the public order uid.

Request

  • uid
    In
    path
    Type
    string
    Required
    Required
    Description
    Order.uid.
  • Response: { order } with line items.
  • Buyer billing fields may be redacted under marketplace-of-record privacy rules.
POST/api/v1/orders/:uid/deliver
orders:write

Mark delivered

Manual fulfillment. COMPLEX lines must be fully attached when required. Emits order.delivered.

Request

  • uid
    In
    path
    Type
    string
    Required
    Required
    Description
    Order.uid.
  • evidence
    In
    body
    Type
    string[]
    Required
    Optional
    Limits
    HTTPS, max 10
    Description
    Optional screenshot or transfer-proof URLs.
  • Response: { success: true, order }.
  • COMPLEX inventory lines must be fully attached before deliver when the product requires it.
  • Emits order.delivered webhook when configured.

Hosted checkout

Any approved partner shop or backend can send buyers to an RMT.GG pay page. We stay merchant of record and take 4% of the locked amount.

Allowlist and fulfillment

Apply under Settings, Hosted checkout, then create an API key and JSON webhook there. After payment we emit checkout.completed. Delivery values stay on the RMT.GG confirmation; they are not in seller GET or webhooks.

POST/api/v1/checkout/sessions
checkout:write

Create a hosted checkout session

Send buyers to a locked RMT.GG pay page. One item: amount and itemName. Cart: items[] with name and amount on each line. Currency defaults to USD. After payment the buyer stays on RMT.GG when there are delivery fields to copy. returnUrl continues to the shop; with no delivery we send them back after a short countdown. Amount, lengths, and other caps are in the Limits column.

javascript
{
  amount:   10,           // what the buyer pays
  itemName: "Gold pack",  // pay page heading
}

Request

  • amount
    In
    body
    Type
    number
    Required
    Conditional
    Limits
    > 0, max 1,000,000
    Description
    What the buyer pays. Required for a single item. With items[], omit it or send the line sum. Mismatch: 400 AMOUNT_MISMATCH.
  • currency
    In
    body
    Type
    string
    Required
    Optional
    Limits
    Default USD
    Description
    ISO 4217 code such as USD or EUR.
  • itemName
    In
    body
    Type
    string
    Required
    Conditional
    Limits
    Max 120
    Description
    Pay page heading. Required for a single item. Alias: title. With items[], defaults to the first line name. Missing: 400 ITEM_NAME_REQUIRED.
  • title
    In
    body
    Type
    string
    Required
    Optional
    Description
    Alias of itemName. If both are sent, itemName wins.
  • description
    In
    body
    Type
    string
    Required
    Optional
    Limits
    Max 200
    Description
    Copy under the heading. If omitted, the heading is reused.
  • imageUrl
    In
    body
    Type
    string
    Required
    Optional
    Limits
    HTTPS, max 2048
    Description
    Product image, or fallback for lines without imageUrl. Invalid: 400 INVALID_IMAGE_URL.
  • items
    In
    body
    Type
    object[]
    Required
    Conditional
    Limits
    1-20 lines, JSON max 48,000
    Description
    Locked cart. Required when amount is omitted. Buyers cannot change lines. Empty: 400 INVALID_ITEMS.
  • items[].name
    In
    body
    Type
    string
    Required
    Required
    Limits
    Max 120
    Description
    Line title. Alias: title.
  • items[].title
    In
    body
    Type
    string
    Required
    Optional
    Description
    Alias of items[].name. If both are sent, name wins.
  • items[].description
    In
    body
    Type
    string
    Required
    Optional
    Limits
    Max 200
    Description
    Line copy under the name.
  • items[].amount
    In
    body
    Type
    number
    Required
    Required
    Limits
    > 0, max 1,000,000
    Description
    Unit price. Session total is sum(amount * quantity).
  • items[].quantity
    In
    body
    Type
    number
    Required
    Optional
    Limits
    1-99, default 1
    Description
    Locked on the pay page.
  • items[].imageUrl
    In
    body
    Type
    string
    Required
    Optional
    Limits
    HTTPS, max 2048
    Description
    Line image. Falls back to top-level imageUrl.
  • items[].delivery
    In
    body
    Type
    object[]
    Required
    Optional
    Limits
    Max 16 fields
    Description
    Shown after payment on RMT.GG. Seller GET and webhooks omit values.
  • items[].delivery[].name
    In
    body
    Type
    string
    Required
    Required
    Limits
    Max 80
    Description
    Field label, for example Code or Password.
  • items[].delivery[].type
    In
    body
    Type
    string
    Required
    Optional
    Limits
    text, password, textarea
    Description
    password is blurred until the buyer reveals it. Default text.
  • items[].delivery[].value
    In
    body
    Type
    string
    Required
    Required
    Limits
    Max 2048
    Description
    Field value. Numbers are stored as strings. Empty: 400 INVALID_DELIVERY.
  • email
    In
    body
    Type
    string
    Required
    Optional
    Limits
    Invalid values ignored
    Description
    Prefills the pay page. The buyer still confirms email before paying.
  • returnUrl
    In
    body
    Type
    string
    Required
    Optional
    Limits
    HTTPS, max 2048
    Description
    Continue-to-shop after payment. Delivery fields keep the buyer on RMT.GG with a button. No delivery: we send them back after a short countdown. If omitted, there is no shop button. http://localhost is allowed for local shops.
  • cancelUrl
    In
    body
    Type
    string
    Required
    Optional
    Limits
    HTTPS, max 2048
    Description
    Redirect if the buyer cancels or the session expires. If omitted, they stay on the pay page.
  • invoiceId
    In
    body
    Type
    string
    Required
    Optional
    Limits
    Max 128
    Description
    Your shop id. Same payload returns the existing session. A different payload: 409 INVOICE_CONFLICT.
  • categorySlug
    In
    body
    Type
    string
    Required
    Conditional
    Limits
    With offering, or omit both
    Description
    Public root slug such as games. Used for card, PayPal, and crypto labels, not the pay page title. Wrong pair: 400 INVALID_PSP_CATEGORY.
  • offering
    In
    body
    Type
    string
    Required
    Conditional
    Limits
    With categorySlug, or omit both
    Description
    Catalog offering such as Mods. Mapped to labels like Games · Add-ons.
  • metadata
    In
    body
    Type
    object
    Required
    Optional
    Limits
    Object, max 4096 chars
    Description
    Stored on the session. Not returned on seller GET.
  • Idempotency-Key
    In
    header
    Type
    string
    Required
    Optional
    Limits
    Max 128
    Description
    Replay header. Same key and payload returns the existing session. A different payload: 409 IDEMPOTENCY_CONFLICT.

Response

  • uid
    In
    response
    Type
    string
    Description
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    In
    response
    Type
    string
    Description
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    In
    response
    Type
    boolean
    Description
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    In
    response
    Type
    number
    Description
    Locked buyer total in major units.
  • currency
    In
    response
    Type
    string
    Description
    ISO currency code stored on the session (for example USD).
  • itemName
    In
    response
    Type
    string | null
    Description
    Pay page heading.
  • description
    In
    response
    Type
    string
    Description
    Longer copy under the heading.
  • email
    In
    response
    Type
    string | null
    Description
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    In
    response
    Type
    string | null
    Description
    Buyer locale when known. Not a create-session field.
  • returnUrl
    In
    response
    Type
    string | null
    Description
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    In
    response
    Type
    string | null
    Description
    Cancel/expiry redirect, or null.
  • invoiceId
    In
    response
    Type
    string | null
    Description
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    In
    response
    Type
    string | null
    Description
    Same as invoiceId (legacy alias).
  • source
    In
    response
    Type
    string
    Description
    How the session was created. API sessions are "api".
  • expiresAt
    In
    response
    Type
    string
    Description
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    In
    response
    Type
    string
    Description
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    In
    response
    Type
    string | null
    Description
    Marketplace order uid after payment. null until the session is paid.
  • items
    In
    response
    Type
    object[]
    Description
    Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values.
  • hosted_url
    In
    response
    Type
    string
    Description
    Pay page URL. Send the buyer here. Same target as hostedUrl.
  • expires_at
    In
    response
    Type
    string
    Description
    ISO timestamp. Same value as expiresAt.
  • Approved partners only. Platform fee is 4% of the locked amount. Payment-method costs are absorbed by the platform.
  • After payment the buyer stays on RMT.GG so they can copy delivery fields. returnUrl is a continue button when delivery is present. With no delivery fields we send them back after a short countdown. If you omit them, they stay on the pay page after payment, cancel, or expiry.
  • Fulfill on checkout.completed. Sessions expire after 24 hours. Buyers cannot change line items. imageUrl must be HTTPS. categorySlug and offering must be sent together (or omit both); a wrong pair returns 400 INVALID_PSP_CATEGORY.
GET/api/v1/checkout/sessions/:uid
checkout:write

Get a hosted checkout session

Returns the session you created. Use this if checkout.completed is delayed. paid is true only when status is paid. items never include delivery values.

Request

  • uid
    In
    path
    Type
    string
    Required
    Required
    Description
    Session uid returned at create time.

Response

  • uid
    In
    response
    Type
    string
    Description
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    In
    response
    Type
    string
    Description
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    In
    response
    Type
    boolean
    Description
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    In
    response
    Type
    number
    Description
    Locked buyer total in major units.
  • currency
    In
    response
    Type
    string
    Description
    ISO currency code stored on the session (for example USD).
  • itemName
    In
    response
    Type
    string | null
    Description
    Pay page heading.
  • description
    In
    response
    Type
    string
    Description
    Longer copy under the heading.
  • email
    In
    response
    Type
    string | null
    Description
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    In
    response
    Type
    string | null
    Description
    Buyer locale when known. Not a create-session field.
  • returnUrl
    In
    response
    Type
    string | null
    Description
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    In
    response
    Type
    string | null
    Description
    Cancel/expiry redirect, or null.
  • invoiceId
    In
    response
    Type
    string | null
    Description
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    In
    response
    Type
    string | null
    Description
    Same as invoiceId (legacy alias).
  • source
    In
    response
    Type
    string
    Description
    How the session was created. API sessions are "api".
  • expiresAt
    In
    response
    Type
    string
    Description
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    In
    response
    Type
    string
    Description
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    In
    response
    Type
    string | null
    Description
    Marketplace order uid after payment. null until the session is paid.
  • items
    In
    response
    Type
    object[]
    Description
    Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values.
  • Response: { session }. Stale unpaid sessions are marked expired before they are returned.
  • Use this as a backup to checkout.completed. paid is true only when status is paid.
  • 404 NOT_FOUND if the uid is unknown or belongs to another seller.
GET/api/v1/checkout/sessions
checkout:write

Look up a hosted checkout session by invoice id

Same session object as GET by uid. Pass the invoiceId you sent at create. Missing: 400 INVOICE_ID_REQUIRED. Unknown: 404 NOT_FOUND.

Request

  • invoiceId
    In
    query
    Type
    string
    Required
    Required
    Limits
    Max 128
    Description
    invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID.

Response

  • uid
    In
    response
    Type
    string
    Description
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    In
    response
    Type
    string
    Description
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    In
    response
    Type
    boolean
    Description
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    In
    response
    Type
    number
    Description
    Locked buyer total in major units.
  • currency
    In
    response
    Type
    string
    Description
    ISO currency code stored on the session (for example USD).
  • itemName
    In
    response
    Type
    string | null
    Description
    Pay page heading.
  • description
    In
    response
    Type
    string
    Description
    Longer copy under the heading.
  • email
    In
    response
    Type
    string | null
    Description
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    In
    response
    Type
    string | null
    Description
    Buyer locale when known. Not a create-session field.
  • returnUrl
    In
    response
    Type
    string | null
    Description
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    In
    response
    Type
    string | null
    Description
    Cancel/expiry redirect, or null.
  • invoiceId
    In
    response
    Type
    string | null
    Description
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    In
    response
    Type
    string | null
    Description
    Same as invoiceId (legacy alias).
  • source
    In
    response
    Type
    string
    Description
    How the session was created. API sessions are "api".
  • expiresAt
    In
    response
    Type
    string
    Description
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    In
    response
    Type
    string
    Description
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    In
    response
    Type
    string | null
    Description
    Marketplace order uid after payment. null until the session is paid.
  • items
    In
    response
    Type
    object[]
    Description
    Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values.
  • Same { session } body as GET /api/v1/checkout/sessions/:uid, including the fields above.
  • Prefer this when you stored your own invoice id and not the session uid.
  • 404 NOT_FOUND if no session exists for that invoice id.

Outbound webhooks

Configure HTTPS endpoints in Settings → Hosted checkout (or Developer). RMT POSTs when subscribed events fire.

order.paid
order.delivered
order.completed
order.refunded
order.disputed
offer.published
offer.updated
checkout.completed
checkout.canceled
checkout.refunded
  • JSON format posts a structured envelope with id, type, created, and data.
  • Discord format posts rich embeds with order or offer links.
  • Optional signing uses X-RMT-Timestamp and X-RMT-Signature (same scheme as reserve).
  • Delivery history appears under each endpoint so you can retry failures. Endpoints auto-pause after repeated failures.
  • Hosted checkout sends checkout.completed, checkout.canceled, and checkout.refunded with data.checkout. Delivery values are omitted. Marketplace sales keep order.paid and other order.* events.

JSON delivery envelope

json
{
  "id": "whd_…",
  "type": "order.paid",
  "created": "2026-07-23T12:00:00.000Z",
  "data": {
    "order": {
      "uid": "ord_…",
      "reference": "RMT-…",
      "status": "PAID",
      "url": "https://rmt.gg/orders/ord_…",
      "items": [ /* line items with offer names */ ]
    }
  }
}

Signed delivery headers

json
{
  "X-RMT-Event": "order.paid",
  "X-RMT-Delivery": "whd_…",
  "X-RMT-Timestamp": "1710000000",
  "X-RMT-Signature": "v1=abc123…"
}

checkout.completed payload

json
{
  "id": "whd_…",
  "type": "checkout.completed",
  "created": "2026-08-14T12:00:00.000Z",
  "data": {
    "checkout": {
      "uid": "pcs_…",
      "status": "paid",
      "amount": 10,
      "currency": "USD",
      "itemName": "Gold pack",
      "description": "1000 gold for account example",
      "invoiceId": "inv-12345",
      "source": "api",
      "orderUid": "ord_…",
      "hostedUrl": "https://rmt.gg/pay/pcs_…",
      "email": "[email protected]",
      "paidAt": "2026-08-14T12:01:00.000Z",
      "expiresAt": "2026-08-15T12:00:00.000Z",
      "createdAt": "2026-08-14T12:00:00.000Z",
      "reason": null,
      "items": [
        {
          "name": "Gold pack",
          "description": "1000 gold for account example",
          "amount": 10,
          "quantity": 1,
          "imageUrl": "https://cdn.shop.example/gold.png"
        }
      ]
    }
  }
}

checkout.canceled payload

json
{
  "id": "whd_…",
  "type": "checkout.canceled",
  "created": "2026-08-14T12:20:00.000Z",
  "data": {
    "checkout": {
      "uid": "pcs_…",
      "status": "canceled",
      "amount": 10,
      "currency": "USD",
      "itemName": "Gold pack",
      "description": "1000 gold for account example",
      "invoiceId": "inv-12345",
      "source": "api",
      "orderUid": null,
      "hostedUrl": "https://rmt.gg/pay/pcs_…",
      "email": "[email protected]",
      "paidAt": null,
      "expiresAt": "2026-08-15T12:00:00.000Z",
      "createdAt": "2026-08-14T12:00:00.000Z",
      "reason": "buyer_canceled",
      "items": [
        {
          "name": "Gold pack",
          "description": "1000 gold for account example",
          "amount": 10,
          "quantity": 1,
          "imageUrl": "https://cdn.shop.example/gold.png"
        }
      ]
    }
  }
}

Verify request signatures

Used for outbound webhooks and on-demand inventory (reserve) calls when you set a signing secret.

The secret stays on RMT. Each signed POST includes X-RMT-Timestamp (Unix seconds) and X-RMT-Signature (v1= plus hex). Compute HMAC-SHA256 over the string timestamp + '.' + rawBody using your secret, then compare to the hex after v1=. Reject timestamps older than 5 minutes.

  • Read the raw body bytes exactly as received. Do not parse JSON and re-serialize before hashing.
  • Use the X-RMT-Timestamp header value as the timestamp prefix (same string, not reformatted).
  • Compare with a timing-safe equality check. Reject requests with missing or mismatched signatures when a secret is configured.
  • Reject timestamps older than 5 minutes to limit replay. The same scheme applies to reserve.item and outbound order or checkout events.

TypeScript verification (timing-safe compare and 5-minute replay window)

typescript
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_AGE_SEC = 5 * 60; // reject replays older than 5 minutes

export function verifyRmtSignature(opts: {
  secret: string;
  timestamp: string | null | undefined;
  signatureHeader: string | null | undefined;
  rawBody: string; // exact POST bytes. Do not JSON.parse then re-stringify.
  nowSec?: number;
}): boolean {
  const secret = opts.secret.trim();
  const timestamp = String(opts.timestamp ?? "").trim();
  const provided = String(opts.signatureHeader ?? "").trim().replace(/^v1=/i, "");
  if (!secret || !timestamp || !provided) return false;

  const ts = Number(timestamp);
  if (!Number.isInteger(ts) || ts <= 0) return false;
  const nowSec = opts.nowSec ?? Math.floor(Date.now() / 1000);
  if (Math.abs(nowSec - ts) > MAX_AGE_SEC) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${opts.rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(provided.toLowerCase(), "utf8");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

// Express / Node HTTP example:
// const rawBody = (req as { rawBody?: string }).rawBody
//   ?? JSON.stringify(req.body); // only if you captured the raw string first
// const ok = verifyRmtSignature({
//   secret: process.env.RMT_WEBHOOK_SECRET!,
//   timestamp: req.headers["x-rmt-timestamp"] as string,
//   signatureHeader: req.headers["x-rmt-signature"] as string,
//   rawBody,
// });
// if (!ok) return res.status(401).end();

TypeScript webhook handler

typescript
type CheckoutCompleted = {
  id: string;
  type: "checkout.completed";
  created: string;
  data: {
    checkout: {
      uid: string;
      status: "paid";
      amount: number;
      currency: string;
      itemName: string | null;
      description: string;
      invoiceId: string | null;
      source: string | null;
      orderUid: string | null;
      hostedUrl: string;
      email: string | null;
      paidAt: string | null;
      expiresAt: string | null;
      createdAt: string | null;
      reason: null;
      items: Array<{
        name: string;
        description: string | null;
        amount: number;
        quantity: number;
        imageUrl: string | null;
      }>;
    };
  };
};

type CheckoutCanceled = {
  type: "checkout.canceled";
  data: {
    checkout: {
      uid: string;
      status: "canceled" | "expired";
      invoiceId: string | null;
      reason: "buyer_canceled" | "expired";
    };
  };
};

export async function handleRmtWebhook(rawBody: string, headers: Headers) {
  const ok = verifyRmtSignature({
    secret: process.env.RMT_WEBHOOK_SECRET!,
    timestamp: headers.get("x-rmt-timestamp"),
    signatureHeader: headers.get("x-rmt-signature"),
    rawBody,
  });
  if (!ok) throw new Response("Unauthorized", { status: 401 });

  const event = JSON.parse(rawBody) as { type: string; data: Record<string, unknown> };
  switch (event.type) {
    case "checkout.completed": {
      const checkout = (event as CheckoutCompleted).data.checkout;
      if (!checkout.invoiceId || !checkout.paidAt) break;
      await fulfillShopOrder(checkout.invoiceId, checkout.orderUid);
      break;
    }
    case "checkout.canceled": {
      const checkout = (event as CheckoutCanceled).data.checkout;
      await markShopOrderCanceled(checkout.invoiceId, checkout.reason);
      break;
    }
    case "checkout.refunded":
    case "order.paid":
    case "order.delivered":
      break;
    default:
      break;
  }
}

On-demand inventory endpoint

For COMPLEX (unique unit) listings, RMT POSTs your HTTPS API after payment to fetch the next license, account, or key when local stock is short.

Payment-safe failures

If your endpoint times out or returns invalid data, the order stays PAID. The buyer is charged; you see an error on the order and can retry reserve or attach keys manually.

How to set it up

  1. Create a COMPLEX offer with item fields (for example License).
  2. On the Items step, enable the on-demand inventory endpoint and paste your public HTTPS URL.
  3. Optionally set a signing secret so RMT sends X-RMT-Timestamp and X-RMT-Signature on every call.
  4. Run Test (or paste sample JSON), map response paths to item fields, then Save.
  5. Publish the listing. Buyers can purchase with empty local stock; keys are created after payment.
  • Local stock is always preferred; the endpoint fills only the shortfall.
  • Configure an offer-level default, or override per pricing option, on the Items step.
  • HTTPS only. Optional HMAC signing uses the same headers as outbound webhooks (X-RMT-Event: reserve.item).
  • Test in the editor sends dryRun: true. On the order page, use Retry endpoint after fixing your API.

Canonical POST body (truncated)

json
{
  "id": "rsv_…",
  "type": "reserve.item",
  "order": { "uid": "ord_…", "reference": "RMT-…", "url": "https://rmt.gg/orders/ord_…" },
  "offer": { "url": "my-offer", "title": "Game key", "pageUrl": "https://rmt.gg/offers/my-offer" },
  "option": { "id": 1, "name": "Standard" },
  "fields": [{ "id": 10, "name": "License", "type": "text", "required": true }],
  "quantity": 1
}

Request headers (when a signing secret is set)

json
{
  "Content-Type": "application/json",
  "X-RMT-Event": "reserve.item",
  "X-RMT-Delivery": "rsv_…",
  "X-RMT-Timestamp": "1710000000",
  "X-RMT-Signature": "v1=abc123…"
}

Convenience response

json
{
  "entries": [
    { "name": "License", "value": "AAAA-BBBB-CCCC" }
  ]
}

Mapped JSON fields (with responseMap paths like $.license)

json
{
  "license": "AAAA-BBBB-CCCC",
  "email": "[email protected]",
  "password": "temporary-pass"
}

How to verify the signing secret

If you set a secret on the offer, every reserve POST is signed. Recompute HMAC-SHA256(secret, timestamp + '.' + rawBody) and compare to X-RMT-Signature after stripping the v1= prefix. The secret itself is never included in the request.

See full verification example

TypeScript reserve handler (verify, then return entries)

typescript
type ReserveRequest = {
  id: string;
  type: "reserve.item";
  dryRun?: boolean;
  quantity: number;
  fields: Array<{ name: string; required?: boolean }>;
};

export async function handleReserve(rawBody: string, headers: Headers) {
  const ok = verifyRmtSignature({
    secret: process.env.RMT_RESERVE_SECRET!,
    timestamp: headers.get("x-rmt-timestamp"),
    signatureHeader: headers.get("x-rmt-signature"),
    rawBody,
  });
  if (!ok) return new Response("Unauthorized", { status: 401 });

  const body = JSON.parse(rawBody) as ReserveRequest;
  if (body.type !== "reserve.item") {
    return Response.json({ error: "Unexpected event" }, { status: 400 });
  }

  const qty = Number(body.quantity);
  if (!Number.isInteger(qty) || qty < 1) {
    return Response.json({ error: "Invalid quantity" }, { status: 400 });
  }

  if (body.dryRun) {
    return Response.json({
      entries: [{ name: "License", value: "TEST-AAAA-BBBB" }],
    });
  }

  const license = await mintLicense(); // your inventory
  return Response.json({
    entries: [{ name: "License", value: license }],
  });
}

Do not call reserve before payment

RMT only calls your endpoint after payment succeeds, so abandoned checkouts do not burn licenses.

Errors and rate limits

Errors return JSON { error, code? }. Open API traffic is limited to 300 requests per minute per API key.

  • API_KEY_REQUIRED
    401

    Missing Authorization or X-Api-Key header.

  • API_KEY_INVALID
    401

    Key unknown, revoked, expired, or developer access suspended.

  • SCOPE_MISSING
    403

    Key lacks the scope required by the endpoint.

  • RATE_LIMITED
    429

    Too many requests. Honor Retry-After and X-RateLimit-Reset.

  • CHECKOUT_PARTNER_NOT_APPROVED
    403

    This seller is not approved for hosted checkout.

  • INVALID_JSON
    400

    Request body must be JSON.

  • INVALID_AMOUNT
    400

    amount must be greater than 0 and at most 1,000,000.

  • UNSUPPORTED_CURRENCY
    400

    currency is not a supported ISO code.

  • INVALID_RETURN_URL
    400

    returnUrl and cancelUrl must be https (http://localhost is allowed for local shops).

  • INVALID_PSP_CATEGORY
    400

    categorySlug and offering must be sent together and match a public catalog pair. Omit both to use your Hosted checkout default. A wrong pair returns 400 and does not create a session.

  • INVALID_INVOICE_ID
    400

    invoiceId is longer than 128 characters.

  • INVOICE_ID_REQUIRED
    400

    GET /checkout/sessions requires invoiceId as a query parameter.

  • INVALID_IDEMPOTENCY_KEY
    400

    Idempotency-Key is longer than 128 characters.

  • INVALID_METADATA
    400

    metadata must be a JSON object, not an array or primitive.

  • METADATA_TOO_LARGE
    400

    Serialized metadata is larger than 4096 characters.

  • IDEMPOTENCY_CONFLICT
    409

    Idempotency-Key was reused with a different amount, currency, or item.

  • INVOICE_CONFLICT
    409

    invoiceId was reused with a different amount, currency, or item.

  • INVALID_IMAGE_URL
    400

    imageUrl must be an https URL.

  • ITEM_NAME_REQUIRED
    400

    itemName (or title) is required when items is omitted.

  • INVALID_ITEMS
    400

    items must be a non-empty array of locked line items (max 20). Each line needs name and amount.

  • TOO_MANY_ITEMS
    400

    items cannot contain more than 20 lines.

  • AMOUNT_MISMATCH
    400

    amount must equal the sum of each line amount times quantity.

  • INVALID_DELIVERY
    400

    delivery fields are invalid. Each field needs a name (max 80) and value (max 2048). type must be text, password, or textarea (default text). Max 16 fields per line.

  • ITEMS_TOO_LARGE
    400

    Serialized items JSON is larger than 48,000 characters.

  • NOT_FOUND
    404

    No hosted checkout session matches that uid or invoiceId for this seller.

  • RESERVE_FAILED
    400

    On-demand inventory call timed out, returned invalid data, or missed required fields.

  • OPTION_AMBIGUOUS
    409

    More than one pricing option matches that name. Pass optionId from GET stock.

  • OPTION_NOT_FOUND
    404

    No pricing option matches that id or name on this listing.

  • OPTION_REQUIRED
    400

    This listing has multiple pricing options. Pass option or optionId.

  • UNKNOWN_FIELD
    400

    A field name does not match this listing's delivery schema.

  • FIELD_MAPPING_AMBIGUOUS
    400

    Could not map columns or keys to delivery fields. Send headers, or use items objects keyed by field name.

  • STOCK_MODE_MISMATCH
    400

    That payload does not match the option's stock mode (quantity vs saved items).

  • IMPORT_TOO_LARGE
    400

    A restock request may import at most 1,000 saved items per option.

  • OPTION_ITEM_CAPACITY
    400

    This pricing option already has the maximum of 5,000 unsold saved items.

  • DUPLICATE_ITEMS
    409

    onDuplicate=error and at least one item already exists on this option.

  • UNLIMITED_STOCK
    400

    This option has unlimited quantity. Use set to switch to a finite count first.

  • INSUFFICIENT_STOCK
    400

    Not enough quantity stock to remove.

  • STOCK_HELD_IN_CHECKOUT
    400

    Cannot lower quantity below units currently reserved in checkout.

  • INVALID_RESTOCK
    400

    The restock body is missing a required action, or combines add/items in one option.

Handle 429

Back off using Retry-After seconds. Do not rotate keys to bypass limits; the limit is per key and flat for all sellers.

Successful responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.

Ready to automate?

Create a key in Developer settings, and connect Discord or Telegram under Notifications.