RMT.GG/Dokumentation für Entwickler von Verkäufern
v1

Verkäufer API

Automatisiere Angebote, erfülle Verkäufe und streamen Ereignisse von Bestellungen. Beinhaltet ausgehende Webhooks und Reservierungsendpunkte für die Nachfüllung von Beständen nach der Zahlung.

REST Open API

Bearer-authentifiziert /api/v1 für Angebote und Bestellungen, mit Entdeckungs- und Ratenlimit-Headern.

Ausgehende Webhooks

Signierte HTTPS (oder Discord) Lieferungen für Ereignisse im Lebenszyklus von Bestellungen und Angeboten.

Reservieren / Nachfüllen

Mint COMPLEX-Bestände von deinem Server nach der Zahlung, wenn der lokale Bestand knapp ist.

Was du erstellen kannst

Die Verkäufer Open API ist für Verkäufer, die Discord-Benachrichtigungen, Bestands-Synchronisation, Zapier-ähnliche Automatisierung oder ein benutzerdefiniertes Backoffice auf RMT.GG wünschen.

  • Angebote verwalten
    Entwürfe erstellen, sichere Felder aktualisieren, veröffentlichen und archivieren über /api/v1/offers.
  • Verkäufe erfüllen
    Verkäuferbestellungen auflisten und inspizieren, dann als geliefert markieren mit optionalen Beweis-URLs.
  • Unter dem Limit bleiben
    Jeder Schlüssel ist auf 300 Anfragen pro Minute begrenzt. Antworten enthalten X-RateLimit-* Header.
  • In Echtzeit reagieren
    Abonniere Ereignisse von Bestellungen und Angeboten oder fülle COMPLEX-Bestände mit reservierten Webhooks auf.
  • Zahlungen aus deinem Shop annehmen
    Genehmigte Partner können Käufer von einem externen Shop zur gehosteten Kasse senden und dann die Bestellung erfüllen, wenn sie bezahlt ist.

Schnellstart

Erstelle einen API-Schlüssel in den Entwicklereinstellungen und rufe dann Discovery auf, um den Live-Katalog anzuzeigen.

  1. 1Öffne Einstellungen → Entwickler (kein separater Aktivierungsschritt).
  2. 2Erstelle einen API-Schlüssel und kopiere das Geheimnis einmal (rmt_sk_live_…). Speichere es in deinem Geheimnismanager.
  3. 3Rufe GET /api/v1 mit Authorization: Bearer auf, um Scopes, Quoten und Operationen zu bestätigen.
GET/api/v1

Entdeckungsdokument

Gibt Scopes, Quoten, Webhook-Ereignisse und das vollständige Katalog der Operationen zurück. Jeder gültige API-Schlüssel funktioniert.

Authentifizierung

Sende deinen Live-Geheimschlüssel bei jeder /api/v1-Anfrage. Bevorzuge nur HTTPS. Betten Sie niemals Schlüssel in öffentlichen Clients oder Browser-Bundles ein.

Bevorzugter Header

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

Alternativer Header

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

Wiederverwendbarer TypeScript-Client (Bearer-Auth, typisierte Fehler, 429 Wiederholung)

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));
    }
  }
}

Rotieren bei Leck

Wenn ein Schlüssel geleakt wird, widerrufe ihn in den Entwicklereinstellungen und erstelle einen neuen. Aktualisiere deine Automatisierung, bevor du widerrufst, wenn du live bist.

Scopes

Jeder API-Schlüssel trägt Scopes, die Endpunkte steuern. Fehlender Scope gibt 403 SCOPE_MISSING zurück.

offers:read
offers:write
orders:read
orders:write
webhooks:manage
checkout:write
  • offers:read: Liste und hole deine Angebote.
  • offers:write: Erstelle, aktualisiere, veröffentliche und lösche Angebote.
  • orders:read: Liste und hole Verkäuferbestellungen.
  • orders:write: Markiere Bestellungen als geliefert.
  • webhooks:manage: Reserviert für zukünftige Open API Webhook-Verwaltung. Konfiguriere Discord/Telegram in den Benachrichtigungen und JSON-Webhooks in den Entwicklereinstellungen heute.
  • checkout:write: Erstellen und Lesen von gehosteten Checkout-Sessions. Erfordert die Genehmigung des Administrators für Partner-Checkout.

Standard-Schlüssel-Scope

Neue Schlüssel erhalten offers:read, offers:write, orders:read und orders:write. Ausgehende Webhook CRUD bleibt in der UI der Einstellungen (Sitzungsauthentifizierung).

Angebote API

Angebotsidentifikatoren akzeptieren den öffentlichen URL-Slug oder die numerische ID. Antworten lassen interne ID und sellerId weg.

Was PATCH noch nicht ändern kann

Bestandszeilen, Optionspreise, Medien und Attribute werden im Verkäufer-Editor (oder zukünftigen Endpunkten) verwaltet, nicht über PATCH heute.

GET/api/v1/offers
offers:read

Liste deine Angebote

Filtern mit archive=active (Standard), archiviert oder alle.

Anfrage

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

Erstelle ein Entwurfsangebot

Erstellt einen leeren Entwurf, der dem authentifizierten Verkäufer gehört. Kein Körper erforderlich.

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

Hole ein Angebot

Lade nach öffentlichem URL-Slug oder numerischer ID. Beziehungen (Optionen) können enthalten sein; Bestandsartikel sind nicht enthalten.

Anfrage

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

Aktualisiere Angebotsfelder

Patch ein sicheres Teilset von Angebotsfeldern. Gibt offer.updated aus, wenn ausgehende Webhooks konfiguriert sind.

Anfrage

  • urlOrId
    In
    path
    Typ
    string
    Erforderlich
    erforderlich
    Beschreibung
    Offer.url slug or Offer.id.
  • title
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    Listing title.
  • description
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    Listing description.
  • visibility
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    PUBLIC | PRIVATE | UNPUBLISHED.
  • categoryId
    In
    body
    Typ
    number
    Erforderlich
    Optional
    Beschreibung
    Catalog category id.
  • offeringId
    In
    body
    Typ
    number
    Erforderlich
    Optional
    Beschreibung
    Catalog offering id.
  • thumbnail
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    Thumbnail URL or asset reference.
  • offerType
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    Offer type string used by the listing.
  • listingMode
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    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

Löschen oder archivieren

Die gleichen Lösch-/Archivierungsregeln wie die Verkäufer-UI.

Anfrage

  • urlOrId
    In
    path
    Typ
    string
    Erforderlich
    erforderlich
    Beschreibung
    Offer.url slug or Offer.id.
  • Response: { ok: true }.
POST/api/v1/offers/:urlOrId/publish
offers:write

Veröffentliche ein Angebot

Veröffentlicht einen Entwurf (oder ändert die Sichtbarkeit). Schlägt mit 400 fehl, wenn erforderliche Angebotsfelder unvollständig sind.

Anfrage

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

Bestand API

Siehe kaufbare Mengen pro Stufe, ordne die Lieferfeldnamen dem richtigen Angebot zu und fülle dann die Menge oder die gespeicherten Schlüssel und Konten nach.

So funktioniert das Matching

GET /api/v1/stock?fields=username,password findet Angebote, deren Schema diese Felder hat. Nachfüllen mit Optionsnamen (oder optionId) und Feldnamen. Interne Feld-IDs sind nicht erforderlich. Antworten enthalten niemals Anmeldeinformationen.

GET/api/v1/stock
offers:read

Bestand über Ihre Angebote auflisten

Gibt die Mengen pro Stufe und die Lieferfeldnamen zurück, damit Sie Schlüssel und Konten dem richtigen Angebot zuordnen können. Filtern Sie mit q, fields, stockMode und lowStock. Gibt niemals Anmeldeinformationen zurück.

Anfrage

  • q
    In
    query
    Typ
    string
    Erforderlich
    Optional
    Limits
    Max 80
    Beschreibung
    Filter by listing title or url slug.
  • fields
    In
    query
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    Comma-separated delivery field names. The listing must have all of them (Username,Password). Names match case-insensitively.
  • stockMode
    In
    query
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    QUANTITY or COMPLEX. Listing must have at least one option in that mode.
  • lowStock
    In
    query
    Typ
    number
    Erforderlich
    Optional
    Beschreibung
    Keep listings that have a finite tier with available less than or equal to this number.
  • archive
    In
    query
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    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

Bestand für ein Angebot abrufen

Gleiche StockOffer-Form wie der Index, für eine URL oder numerische ID. Nur Mengen.

Anfrage

  • urlOrId
    In
    path
    Typ
    string
    Erforderlich
    erforderlich
    Beschreibung
    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

Ein Angebot nachfüllen

Mengenstufen: hinzufügen, entfernen oder festlegen. Gespeicherte Artikelstufen: Artikelobjekte nach Feldnamen, keys[], wenn es ein Feld gibt, oder durch Text getrennt. Mehrere Stufen in einem Aufruf über options[]. dryRun zeigt eine Vorschau des Matchings. onDuplicate standardmäßig auf überspringen.

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

Anfrage

  • urlOrId
    In
    path
    Typ
    string
    Erforderlich
    erforderlich
    Beschreibung
    Offer.url slug or Offer.id.
  • option
    In
    body
    Typ
    string
    Erforderlich
    Bedingt
    Beschreibung
    Pricing option name (case-insensitive). Omit when the listing has a single tier.
  • optionId
    In
    body
    Typ
    number
    Erforderlich
    Bedingt
    Beschreibung
    Pricing option id from GET stock. Wins over option when both are sent. Ambiguous names return 409 OPTION_AMBIGUOUS.
  • add
    In
    body
    Typ
    number
    Erforderlich
    Bedingt
    Limits
    1-1,000,000
    Beschreibung
    QUANTITY: add this many units. Fails with 400 UNLIMITED_STOCK if the tier is unlimited.
  • remove
    In
    body
    Typ
    number
    Erforderlich
    Bedingt
    Limits
    1-1,000,000
    Beschreibung
    QUANTITY: withdraw this many units. Fails with 400 INSUFFICIENT_STOCK when there is not enough.
  • set
    In
    body
    Typ
    number | null
    Erforderlich
    Bedingt
    Beschreibung
    QUANTITY: set an absolute count. null means unlimited. Cannot go below units held in checkout.
  • items
    In
    body
    Typ
    object[]
    Erforderlich
    Bedingt
    Limits
    Max 1,000
    Beschreibung
    COMPLEX: objects keyed by delivery field name, for example { "Username": "a", "Password": "b" }. Names match case-insensitively.
  • keys
    In
    body
    Typ
    string[]
    Erforderlich
    Bedingt
    Limits
    Max 1,000
    Beschreibung
    COMPLEX: license keys when the listing has exactly one delivery field. Otherwise 400 FIELD_MAPPING_AMBIGUOUS.
  • text
    In
    body
    Typ
    string
    Erforderlich
    Bedingt
    Limits
    Max 1,000 rows
    Beschreibung
    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
    Typ
    string
    Erforderlich
    Optional
    Limits
    Default :
    Beschreibung
    Delimiter for text. Ignored unless text is sent.
  • headers
    In
    body
    Typ
    string[]
    Erforderlich
    Optional
    Beschreibung
    Optional column headers for text when the first line is data, not names.
  • options
    In
    body
    Typ
    object[]
    Erforderlich
    Bedingt
    Beschreibung
    Restock several tiers in one call. Each element is the same shape as a single-option body (option, add, items, …).
  • dryRun
    In
    body
    Typ
    boolean
    Erforderlich
    Optional
    Beschreibung
    Preview matching and counts without writing. Default false.
  • onDuplicate
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    skip (default) or error
    Beschreibung
    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.

Viele Konten oder Schlüssel importieren

Leitfaden

Verwende POST /api/v1/offers/:url/stock mit items[] für Konten oder keys[] für Lizenzcodes mit einem Feld. In 1.000 Zeilen pro Anfrage aufteilen.

  1. 1HOL DIR das Angebot. Verwende fields[] und stockMode, um Gegenstände, Schlüssel oder Hinzufügungen auszuwählen.
  2. 2Speichere Konten als JSON oder CSV, sortiert nach Feldnamen. Speichere Lizenzschlüssel jeweils in einer Zeile.
  3. 3Führe zuerst einen Testlauf durch. Überprüfe wouldImport, skippedDuplicates und matchedFields.
  4. 4POST den gleichen Body erneut ohne dryRun, um den Bestand zu schreiben.

Wähle die Nutzlast, die zur Anzeige passt

Rufe zuerst GET stock auf. Wenn fields[] mehr als einen Namen hat, sende items-Objekte, die nach diesen Namen (Benutzername, Passwort, E-Mail) benannt sind. Wenn es genau ein Feld gibt, reicht keys[]. Mengenangaben verwenden add, nicht items.

1.000 Zeilen pro Anfrage. 5.000 unverkaufte Gegenstände pro Stufe. 300 Anfragen pro Minute. Duplikate werden standardmäßig übersprungen.

accounts.json (ein Objekt pro Konto)

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

accounts.csv

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

keys.txt (ein Lizenzschlüssel pro Zeile)

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 Batch-Import (1.000 Zeilen pro Anfrage)

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) }),
      }),
    );
  }
}

Was diese API schützt

Schlüssel benötigen offers:write, sind rate-limitiert und können nur deine eigenen Angebote nachfüllen. GET gibt niemals gespeicherte Anmeldeinformationen zurück. POST-Antworten geben Benutzername, Passwort oder Schlüsselwerte nicht wieder. Sende den Body über HTTPS in der Produktion und halte den API-Schlüssel in einer Umgebungsvariable.

Unter Windows, benutze curl.exe (nicht den curl Alias). Setze den -d JSON in Anführungszeichen, damit PowerShell ihn nicht aufteilt.

Bestellungen API

Bestellungen sind auf dein Verkäuferkonto beschränkt. Käuferabrechnungsdetails können gemäß den Datenschutzbestimmungen des Marktplatzes anonymisiert werden.

GET/api/v1/orders
orders:read

Liste Verkäuferbestellungen

Unterstützt limit, offset, status, q und sort (neueste, älteste, total_high, total_low).

Anfrage

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

Hole eine Bestellung

Gibt die Bestellung mit Positionen zurück. Verwende die öffentliche Bestell-UID.

Anfrage

  • uid
    In
    path
    Typ
    string
    Erforderlich
    erforderlich
    Beschreibung
    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

Markiere als geliefert

Manuelle Erfüllung. COMPLEX-Positionen müssen vollständig angehängt sein, wenn erforderlich. Gibt order.delivered aus.

Anfrage

  • uid
    In
    path
    Typ
    string
    Erforderlich
    erforderlich
    Beschreibung
    Order.uid.
  • evidence
    In
    body
    Typ
    string[]
    Erforderlich
    Optional
    Limits
    HTTPS, max 10
    Beschreibung
    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.

Gehosteter Checkout

Jeder genehmigte Partner-Shop oder Backend kann Käufer zu einer RMT.GG-Zahlungsseite senden. Wir bleiben der Händler und behalten 4% des gesperrten Betrags.

Whitelist und Erfüllung

Gehe zu Einstellungen, Gehosteter Checkout, und erstelle dort einen API-Schlüssel und ein JSON-Webhooks. Nach der Zahlung senden wir checkout.completed. Lieferwerte bleiben in der RMT.GG-Bestätigung; sie sind nicht im Verkäufer-GET oder in Webhooks enthalten.

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

Erstellen Sie eine gehostete Checkout-Session

Leite Käufer zu einer gesperrten RMT.GG-Zahlungsseite. Ein Artikel: Betrag und itemName. Warenkorb: items[] mit Name und Betrag in jeder Zeile. Die Währung ist standardmäßig USD. Nach der Zahlung bleibt der Käufer auf RMT.GG, wenn es Lieferfelder zum Kopieren gibt. returnUrl führt zurück zum Shop; ohne Lieferung senden wir sie nach einem kurzen Countdown zurück. Beträge, Längen und andere Obergrenzen sind in der Spalte Limits.

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

Anfrage

  • amount
    In
    body
    Typ
    number
    Erforderlich
    Bedingt
    Limits
    > 0, max 1,000,000
    Beschreibung
    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
    Typ
    string
    Erforderlich
    Optional
    Limits
    Default USD
    Beschreibung
    ISO 4217 code such as USD or EUR.
  • itemName
    In
    body
    Typ
    string
    Erforderlich
    Bedingt
    Limits
    Max 120
    Beschreibung
    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
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    Alias of itemName. If both are sent, itemName wins.
  • description
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    Max 200
    Beschreibung
    Copy under the heading. If omitted, the heading is reused.
  • imageUrl
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    HTTPS, max 2048
    Beschreibung
    Product image, or fallback for lines without imageUrl. Invalid: 400 INVALID_IMAGE_URL.
  • items
    In
    body
    Typ
    object[]
    Erforderlich
    Bedingt
    Limits
    1-20 lines, JSON max 48,000
    Beschreibung
    Locked cart. Required when amount is omitted. Buyers cannot change lines. Empty: 400 INVALID_ITEMS.
  • items[].name
    In
    body
    Typ
    string
    Erforderlich
    erforderlich
    Limits
    Max 120
    Beschreibung
    Line title. Alias: title.
  • items[].title
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Beschreibung
    Alias of items[].name. If both are sent, name wins.
  • items[].description
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    Max 200
    Beschreibung
    Line copy under the name.
  • items[].amount
    In
    body
    Typ
    number
    Erforderlich
    erforderlich
    Limits
    > 0, max 1,000,000
    Beschreibung
    Unit price. Session total is sum(amount * quantity).
  • items[].quantity
    In
    body
    Typ
    number
    Erforderlich
    Optional
    Limits
    1-99, default 1
    Beschreibung
    Locked on the pay page.
  • items[].imageUrl
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    HTTPS, max 2048
    Beschreibung
    Line image. Falls back to top-level imageUrl.
  • items[].delivery
    In
    body
    Typ
    object[]
    Erforderlich
    Optional
    Limits
    Max 16 fields
    Beschreibung
    Shown after payment on RMT.GG. Seller GET and webhooks omit values.
  • items[].delivery[].name
    In
    body
    Typ
    string
    Erforderlich
    erforderlich
    Limits
    Max 80
    Beschreibung
    Field label, for example Code or Password.
  • items[].delivery[].type
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    text, password, textarea
    Beschreibung
    password is blurred until the buyer reveals it. Default text.
  • items[].delivery[].value
    In
    body
    Typ
    string
    Erforderlich
    erforderlich
    Limits
    Max 2048
    Beschreibung
    Field value. Numbers are stored as strings. Empty: 400 INVALID_DELIVERY.
  • email
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    Invalid values ignored
    Beschreibung
    Prefills the pay page. The buyer still confirms email before paying.
  • returnUrl
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    HTTPS, max 2048
    Beschreibung
    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
    Typ
    string
    Erforderlich
    Optional
    Limits
    HTTPS, max 2048
    Beschreibung
    Redirect if the buyer cancels or the session expires. If omitted, they stay on the pay page.
  • invoiceId
    In
    body
    Typ
    string
    Erforderlich
    Optional
    Limits
    Max 128
    Beschreibung
    Your shop id. Same payload returns the existing session. A different payload: 409 INVOICE_CONFLICT.
  • categorySlug
    In
    body
    Typ
    string
    Erforderlich
    Bedingt
    Limits
    With offering, or omit both
    Beschreibung
    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
    Typ
    string
    Erforderlich
    Bedingt
    Limits
    With categorySlug, or omit both
    Beschreibung
    Catalog offering such as Mods. Mapped to labels like Games · Add-ons.
  • metadata
    In
    body
    Typ
    object
    Erforderlich
    Optional
    Limits
    Object, max 4096 chars
    Beschreibung
    Stored on the session. Not returned on seller GET.
  • Idempotency-Key
    In
    header
    Typ
    string
    Erforderlich
    Optional
    Limits
    Max 128
    Beschreibung
    Replay header. Same key and payload returns the existing session. A different payload: 409 IDEMPOTENCY_CONFLICT.

Antwort

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

Holen Sie sich eine gehostete Checkout-Session

Gibt die von dir erstellte Sitzung zurück. Verwende dies, wenn checkout.completed verzögert ist. paid ist nur dann true, wenn der Status bezahlt ist. Items enthalten niemals Lieferwerte.

Anfrage

  • uid
    In
    path
    Typ
    string
    Erforderlich
    erforderlich
    Beschreibung
    Session uid returned at create time.

Antwort

  • uid
    In
    response
    Typ
    string
    Beschreibung
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    In
    response
    Typ
    string
    Beschreibung
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    In
    response
    Typ
    boolean
    Beschreibung
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    In
    response
    Typ
    number
    Beschreibung
    Locked buyer total in major units.
  • currency
    In
    response
    Typ
    string
    Beschreibung
    ISO currency code stored on the session (for example USD).
  • itemName
    In
    response
    Typ
    string | null
    Beschreibung
    Pay page heading.
  • description
    In
    response
    Typ
    string
    Beschreibung
    Longer copy under the heading.
  • email
    In
    response
    Typ
    string | null
    Beschreibung
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    In
    response
    Typ
    string | null
    Beschreibung
    Buyer locale when known. Not a create-session field.
  • returnUrl
    In
    response
    Typ
    string | null
    Beschreibung
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    In
    response
    Typ
    string | null
    Beschreibung
    Cancel/expiry redirect, or null.
  • invoiceId
    In
    response
    Typ
    string | null
    Beschreibung
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    In
    response
    Typ
    string | null
    Beschreibung
    Same as invoiceId (legacy alias).
  • source
    In
    response
    Typ
    string
    Beschreibung
    How the session was created. API sessions are "api".
  • expiresAt
    In
    response
    Typ
    string
    Beschreibung
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    In
    response
    Typ
    string
    Beschreibung
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    In
    response
    Typ
    string | null
    Beschreibung
    Marketplace order uid after payment. null until the session is paid.
  • items
    In
    response
    Typ
    object[]
    Beschreibung
    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

Gehostete Checkout-Sitzung nach Rechnungs-ID suchen

Dasselbe Sitzungsobjekt wie GET nach uid. Übergebe die invoiceId, die du bei der Erstellung gesendet hast. Fehlend: 400 INVOICE_ID_REQUIRED. Unbekannt: 404 NOT_FOUND.

Anfrage

  • invoiceId
    In
    query
    Typ
    string
    Erforderlich
    erforderlich
    Limits
    Max 128
    Beschreibung
    invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID.

Antwort

  • uid
    In
    response
    Typ
    string
    Beschreibung
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    In
    response
    Typ
    string
    Beschreibung
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    In
    response
    Typ
    boolean
    Beschreibung
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    In
    response
    Typ
    number
    Beschreibung
    Locked buyer total in major units.
  • currency
    In
    response
    Typ
    string
    Beschreibung
    ISO currency code stored on the session (for example USD).
  • itemName
    In
    response
    Typ
    string | null
    Beschreibung
    Pay page heading.
  • description
    In
    response
    Typ
    string
    Beschreibung
    Longer copy under the heading.
  • email
    In
    response
    Typ
    string | null
    Beschreibung
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    In
    response
    Typ
    string | null
    Beschreibung
    Buyer locale when known. Not a create-session field.
  • returnUrl
    In
    response
    Typ
    string | null
    Beschreibung
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    In
    response
    Typ
    string | null
    Beschreibung
    Cancel/expiry redirect, or null.
  • invoiceId
    In
    response
    Typ
    string | null
    Beschreibung
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    In
    response
    Typ
    string | null
    Beschreibung
    Same as invoiceId (legacy alias).
  • source
    In
    response
    Typ
    string
    Beschreibung
    How the session was created. API sessions are "api".
  • expiresAt
    In
    response
    Typ
    string
    Beschreibung
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    In
    response
    Typ
    string
    Beschreibung
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    In
    response
    Typ
    string | null
    Beschreibung
    Marketplace order uid after payment. null until the session is paid.
  • items
    In
    response
    Typ
    object[]
    Beschreibung
    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.

Ausgehende Webhooks

Konfiguriere HTTPS-Endpunkte (oder Discord-Webhooks) in Einstellungen → Entwickler. RMT POSTs, wenn abonnierte Ereignisse ausgelöst werden.

order.paid
order.delivered
order.completed
order.refunded
order.disputed
offer.published
offer.updated
checkout.completed
checkout.canceled
checkout.refunded
  • JSON-Format sendet einen strukturierten Umschlag mit id, type, created und data.
  • Discord-Format sendet reichhaltige Einbettungen mit Links zu Bestellungen oder Angeboten.
  • Optionale Signierung verwendet X-RMT-Timestamp und X-RMT-Signature (das gleiche Schema wie bei der Reservierung).
  • Die Lieferhistorie erscheint unter jedem Endpunkt, damit du fehlgeschlagene Versuche erneut versuchen kannst. Endpunkte pausieren automatisch nach wiederholten Fehlern.
  • Der gehostete Checkout sendet checkout.completed, checkout.canceled und checkout.refunded mit data.checkout. Lieferwerte werden weggelassen. Marktplatzverkäufe behalten order.paid und andere order.* Ereignisse.

JSON-Lieferumschlag

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 */ ]
    }
  }
}

Signierte Lieferheader

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

checkout.abgeschlossen 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"
        }
      ]
    }
  }
}

Webhook-Signaturen überprüfen

Wenn ein Signierungsgeheimnis festgelegt ist, berechne HMAC-SHA256 über timestamp + '.' + rawBody und vergleiche mit dem Hex nach v1=.

Das Geheimnis bleibt bei RMT. Jeder signierte POST enthält X-RMT-Timestamp (Unix-Sekunden) und X-RMT-Signature (v1= plus hex). Berechne HMAC-SHA256 über den String timestamp + '.' + rawBody mit deinem Geheimnis und vergleiche dann mit dem Hex nach v1=. Lehne Zeitstempel, die älter als 5 Minuten sind, ab.

  • Lese die rohen Body-Bytes genau so, wie sie empfangen wurden. Parste kein JSON und serialisiere nicht neu, bevor du hasht.
  • Verwende den Wert des X-RMT-Timestamp-Headers als Zeitstempel-Präfix (derselbe String, nicht umformatiert).
  • Vergleiche mit einem zeit-sicheren Gleichheitscheck. Lehne Anfragen mit fehlenden oder nicht übereinstimmenden Signaturen ab, wenn ein Geheimnis konfiguriert ist.
  • Lehne Zeitstempel, die älter als 5 Minuten sind, ab, um Wiederholungen zu begrenzen. Das gleiche Schema gilt für reserve.item und ausgehende Bestell- oder Checkout-Ereignisse.

TypeScript-Überprüfung (zeit-sichere Vergleiche und 5-Minuten-Wiederholungsfenster)

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;
  }
}

Webhook reservieren (Bestandsauffüllung)

Für COMPLEX (einzigartige Einheit) Angebote kann RMT dein HTTPS-Endpunkt nach der Zahlung POSTen, um die nächste Lizenz, das Konto oder den Schlüssel zu minten, wenn der lokale Bestand knapp ist.

Zahlungssichere Fehler

Wenn dein Endpunkt zeitlich begrenzt ist oder ungültige Daten zurückgibt, bleibt die Bestellung BEZAHLT. Der Käufer wird belastet; du siehst einen Fehler bei der Bestellung und kannst die Reservierung erneut versuchen oder Schlüssel manuell anhängen.

So richtest du es ein

  1. Erstelle ein KOMPLEXES Angebot mit Artikel-Feldern (zum Beispiel Lizenz).
  2. Aktiviere im Schritt Artikel den On-Demand-Inventar-Endpunkt und füge deine öffentliche HTTPS-URL ein.
  3. Setze optional ein Signing-Geheimnis, damit RMT bei jedem Aufruf X-RMT-Timestamp und X-RMT-Signature sendet.
  4. Führe einen Test durch (oder füge ein Beispiel-JSON ein), mappe die Antwortpfade auf die Artikel-Felder und speichere dann.
  5. Veröffentliche das Angebot. Käufer können mit leerem lokalem Bestand kaufen; Schlüssel werden nach der Zahlung erstellt.
  • Lokaler Bestand hat immer Vorrang; der Webhook füllt nur die Lücke.
  • Konfiguriere einen Standard auf Angebotsebene oder überschreibe pro Preisoption im Schritt 'Artikel' des Angebotseditors.
  • Nur HTTPS. Optionale HMAC-Signierung entspricht ausgehenden Webhooks (X-RMT-Event: reserve.item).
  • Testen im Editor sendet dryRun: true. Auf der Bestellseite verwende 'Retry reserve', nachdem du deinen Endpunkt behoben hast.

Kanonicischer POST-Körper (gekürzt)

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
}

Anforderungsheader (wenn ein Signing-Geheimnis gesetzt ist)

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

Bequeme Antwort

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

Zuordnungs-JSON-Felder (mit responseMap-Pfaden wie $.license)

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

So verifizierst du das Signing-Geheimnis

Wenn du ein Geheimnis für das Angebot festlegst, ist jeder Reserve-POST signiert. Berechne HMAC-SHA256(secret, timestamp + '.' + rawBody) neu und vergleiche mit X-RMT-Signature, nachdem du das v1= Präfix entfernt hast. Das Geheimnis selbst ist niemals in der Anfrage enthalten.

Siehe vollständiges Verifizierungsbeispiel

TypeScript Reserve-Handler (überprüfen und dann Einträge zurückgeben)

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 }],
  });
}

Rufe die Reservierung nicht vor der Zahlung auf

RMT ruft deinen Endpunkt nur nach erfolgreicher Zahlung auf, sodass abgebrochene Bestellungen keine Lizenzen verbrauchen.

Fehler und Ratenlimits

Fehler geben JSON { error, code? } zurück. Open API-Verkehr ist auf 300 Anfragen pro Minute pro API-Schlüssel begrenzt.

  • API_KEY_REQUIRED
    401

    Fehlender Authorization- oder X-Api-Key-Header.

  • API_KEY_INVALID
    401

    Schlüssel unbekannt, widerrufen, abgelaufen oder Entwicklerzugang ausgesetzt.

  • SCOPE_MISSING
    403

    Schlüssel hat nicht den für den Endpunkt erforderlichen Scope.

  • RATE_LIMITED
    429

    Zu viele Anfragen. Beachte Retry-After und X-RateLimit-Reset.

  • CHECKOUT_PARTNER_NOT_APPROVED
    403

    Dieser Verkäufer ist nicht für den gehosteten Checkout genehmigt.

  • INVALID_JSON
    400

    Der Anfragekörper muss JSON sein.

  • INVALID_AMOUNT
    400

    Der Betrag muss größer als 0 und maximal 1.000.000 sein.

  • UNSUPPORTED_CURRENCY
    400

    Die Währung ist kein unterstützter ISO-Code.

  • INVALID_RETURN_URL
    400

    returnUrl und cancelUrl müssen https sein (http://localhost ist für lokale Shops erlaubt).

  • INVALID_PSP_CATEGORY
    400

    categorySlug und offering müssen zusammen gesendet werden und ein Katalogpaar entsprechen.

  • INVALID_INVOICE_ID
    400

    invoiceId ist länger als 128 Zeichen.

  • INVOICE_ID_REQUIRED
    400

    GET /checkout/sessions benötigt invoiceId als Abfrageparameter.

  • INVALID_IDEMPOTENCY_KEY
    400

    Idempotency-Key ist länger als 128 Zeichen.

  • INVALID_METADATA
    400

    metadata muss ein JSON-Objekt sein, kein Array oder primitiver Typ.

  • METADATA_TOO_LARGE
    400

    Serialisierte Metadaten sind größer als 4096 Zeichen.

  • IDEMPOTENCY_CONFLICT
    409

    Idempotency-Key wurde mit einem anderen Betrag, Währung oder Artikel wiederverwendet.

  • INVOICE_CONFLICT
    409

    invoiceId wurde mit einem anderen Betrag, Währung oder Artikel wiederverwendet.

  • INVALID_IMAGE_URL
    400

    imageUrl muss eine https-URL sein.

  • ITEM_NAME_REQUIRED
    400

    itemName (oder Titel) ist erforderlich, wenn items weggelassen wird.

  • INVALID_ITEMS
    400

    items müssen ein nicht leeres Array von gesperrten Positionen (max 20) sein. Jede Zeile benötigt einen Namen und einen Betrag.

  • TOO_MANY_ITEMS
    400

    items dürfen nicht mehr als 20 Zeilen enthalten.

  • AMOUNT_MISMATCH
    400

    Betrag muss der Summe jedes Zeilenbetrags multipliziert mit der Menge entsprechen.

  • INVALID_DELIVERY
    400

    Lieferfelder sind ungültig. Jedes Feld benötigt einen Namen (max 80) und einen Wert (max 2048). type muss text, password oder textarea sein (Standard ist text). Maximal 16 Felder pro Zeile.

  • ITEMS_TOO_LARGE
    400

    Die serialisierten Items im JSON sind größer als 48.000 Zeichen.

  • NOT_FOUND
    404

    Keine gehostete Checkout-Sitzung entspricht dieser uid oder invoiceId für diesen Verkäufer.

  • RESERVE_FAILED
    400

    Webhook-Reservierung hat zeitlich begrenzt, ungültige Daten zurückgegeben oder erforderliche Felder verpasst.

  • OPTION_AMBIGUOUS
    409

    Mehr als eine Preisoption entspricht diesem Namen. Übergeben Sie optionId von GET stock.

  • OPTION_NOT_FOUND
    404

    Keine Preisoption entspricht dieser ID oder diesem Namen in diesem Angebot.

  • OPTION_REQUIRED
    400

    Dieses Angebot hat mehrere Preisoptionen. Übergeben Sie option oder optionId.

  • UNKNOWN_FIELD
    400

    Ein Feldname stimmt nicht mit dem Liefer-Schema dieses Angebots überein.

  • FIELD_MAPPING_AMBIGUOUS
    400

    Konnte Spalten oder Schlüssel nicht den Lieferfeldern zuordnen. Senden Sie Header oder verwenden Sie Artikelobjekte, die nach Feldnamen indiziert sind.

  • STOCK_MODE_MISMATCH
    400

    Diese Nutzlast stimmt nicht mit dem Lager-Modus der Option überein (Menge vs. gespeicherte Artikel).

  • IMPORT_TOO_LARGE
    400

    Eine Nachfüllanfrage kann maximal 1.000 gespeicherte Artikel pro Option importieren.

  • OPTION_ITEM_CAPACITY
    400

    Diese Preisoption hat bereits das Maximum von 5.000 unverkauften gespeicherten Artikeln erreicht.

  • DUPLICATE_ITEMS
    409

    onDuplicate=error und mindestens ein Artikel existiert bereits in dieser Option.

  • UNLIMITED_STOCK
    400

    Diese Option hat eine unbegrenzte Menge. Verwenden Sie set, um zuerst zu einer endlichen Anzahl zu wechseln.

  • INSUFFICIENT_STOCK
    400

    Nicht genügend Mengenbestand zum Entfernen.

  • STOCK_HELD_IN_CHECKOUT
    400

    Die Menge kann nicht unter die aktuell im Checkout reservierten Einheiten gesenkt werden.

  • INVALID_RESTOCK
    400

    Der Nachfüllkörper fehlt eine erforderliche Aktion oder kombiniert add/items in einer Option.

429 behandeln

Reduziere die Nutzung mit Retry-After Sekunden. Drehe keine Schlüssel, um Limits zu umgehen; das Limit gilt pro Schlüssel und ist für alle Verkäufer gleich.

Erfolgreiche Antworten enthalten X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset.

Bereit zu automatisieren?

Erstelle einen Schlüssel in den Entwicklereinstellungen und verbinde Discord oder Telegram unter Benachrichtigungen.