RMT.GG/Dokumen pengembang penjual
v1

API Penjual

Automatisasi daftar, penuhi penjualan, dan alirkan peristiwa pesanan. Termasuk webhook keluar dan endpoint cadangan untuk pengisian ulang inventaris on-demand setelah pembayaran.

REST Open API

Autentikasi Bearer /api/v1 untuk penawaran dan pesanan, dengan header penemuan dan batasan laju.

Webhook keluar

Pengiriman HTTPS (atau Discord) yang ditandatangani untuk peristiwa siklus hidup pesanan dan penawaran.

Cadangkan / isi ulang

Minta stok COMPLEX dari server Anda setelah pembayaran ketika inventaris lokal kurang.

Apa yang bisa Anda bangun

API Terbuka Penjual ditujukan untuk penjual yang ingin mendapatkan pemberitahuan Discord, sinkronisasi stok, otomatisasi gaya Zapier, atau kantor belakang kustom di atas RMT.GG.

  • Kelola penawaran
    Buat draf, perbarui bidang aman, terbitkan, dan arsipkan melalui /api/v1/offers.
  • Penuhi penjualan
    Daftar dan periksa pesanan penjual, lalu tandai sebagai terkirim dengan URL bukti opsional.
  • Tetap di bawah batas
    Setiap kunci dibatasi pada 300 permintaan per menit. Respons menyertakan header X-RateLimit-*.
  • Reaksi secara real-time
    Langganan peristiwa pesanan dan penawaran, atau isi ulang inventaris COMPLEX dengan webhook cadangan.
  • Ambil pembayaran dari toko Anda
    Mitra yang disetujui dapat mengarahkan pembeli dari toko eksternal ke checkout yang dihosting, kemudian memenuhi pesanan.paid.

Mulai cepat

Buat kunci API di pengaturan Developer, lalu panggil discovery untuk mencetak katalog langsung.

  1. 1Buka Pengaturan → Developer (tidak ada langkah aktifasi terpisah).
  2. 2Buat kunci API dan salin rahasia sekali (rmt_sk_live_…). Simpan di pengelola rahasia Anda.
  3. 3Panggil GET /api/v1 dengan Authorization: Bearer untuk mengonfirmasi lingkup, kuota, dan operasi.
GET/api/v1

Dokumen penemuan

Mengembalikan lingkup, kuota, peristiwa webhook, dan katalog operasi lengkap. Setiap kunci API yang valid berfungsi.

Autentikasi

Kirim kunci rahasia langsung Anda di setiap permintaan /api/v1. Utamakan hanya HTTPS. Jangan pernah menyematkan kunci di klien publik atau bundel browser.

Header yang disukai

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

Header alternatif

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

Klien TypeScript yang dapat digunakan kembali (autentikasi Bearer, kesalahan terketik, coba ulang 429)

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

Putar saat kebocoran

Jika kunci bocor, cabut di pengaturan Pengembang dan buat yang baru. Perbarui otomatisasi Anda sebelum mencabut jika Anda sedang aktif.

Lingkup

Setiap kunci API membawa lingkup yang mengatur endpoint. Lingkup yang hilang mengembalikan 403 SCOPE_MISSING.

offers:read
offers:write
orders:read
orders:write
webhooks:manage
checkout:write
  • offers:read: Daftar dan dapatkan penawaran Anda.
  • offers:write: Buat, perbarui, terbitkan, dan hapus penawaran.
  • orders:read: Daftar dan dapatkan pesanan penjual.
  • orders:write: Tandai pesanan terkirim.
  • webhooks:manage: Dikhususkan untuk pengelolaan webhook Open API di masa depan. Konfigurasikan Discord/Telegram di Notifikasi dan webhook JSON di pengaturan Pengembang hari ini.
  • checkout:write: Buat dan baca sesi checkout yang dihosting. Memerlukan persetujuan admin untuk checkout mitra.

Lingkup kunci default

Kunci baru menerima offers:read, offers:write, orders:read, dan orders:write. CRUD webhook keluar tetap di UI Pengaturan (autentikasi sesi).

API Penawaran

Identifikasi penawaran menerima slug URL publik atau id numerik. Respons menghilangkan id internal dan sellerId.

Apa yang tidak bisa diubah oleh PATCH saat ini

Baris stok, harga opsi, media, dan atribut dikelola di editor penjual (atau endpoint di masa depan), bukan melalui PATCH saat ini.

GET/api/v1/offers
offers:read

Daftar penawaran Anda

Saring dengan archive=active (default), diarsipkan, atau semua.

Permintaan

  • archive
    Dalam
    query
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    One of "active" (default), "archived", or "all".
  • Response: { offers: Offer[], total: number }. Numeric id and sellerId are omitted.
POST/api/v1/offers
offers:write

Buat penawaran draf

Membuat draf kosong yang dimiliki oleh penjual yang terautentikasi. Tidak ada badan yang diperlukan.

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

Dapatkan satu penawaran

Muat dengan slug URL publik atau id numerik. Hubungan (opsi) mungkin disertakan; item stok tidak.

Permintaan

  • urlOrId
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    Offer.url slug or Offer.id.
  • Returns relations (options, etc.) when available; items are not included.
PATCH/api/v1/offers/:urlOrId
offers:write

Perbarui bidang penawaran

Patch subset aman dari bidang daftar. Mengeluarkan offer.updated saat webhook keluar dikonfigurasi.

Permintaan

  • urlOrId
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    Offer.url slug or Offer.id.
  • title
    Dalam
    body
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    Listing title.
  • description
    Dalam
    body
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    Listing description.
  • visibility
    Dalam
    body
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    PUBLIC | PRIVATE | UNPUBLISHED.
  • categoryId
    Dalam
    body
    Tipe
    number
    Diperlukan
    Opsional
    Deskripsi
    Catalog category id.
  • offeringId
    Dalam
    body
    Tipe
    number
    Diperlukan
    Opsional
    Deskripsi
    Catalog offering id.
  • thumbnail
    Dalam
    body
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    Thumbnail URL or asset reference.
  • offerType
    Dalam
    body
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    Offer type string used by the listing.
  • listingMode
    Dalam
    body
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    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

Hapus atau arsipkan

Aturan hapus/arsip yang sama seperti UI penjual.

Permintaan

  • urlOrId
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    Offer.url slug or Offer.id.
  • Response: { ok: true }.
POST/api/v1/offers/:urlOrId/publish
offers:write

Terbitkan penawaran

Menerbitkan draf (atau mengubah visibilitas). Gagal dengan 400 jika bidang daftar yang diperlukan tidak lengkap.

Permintaan

  • urlOrId
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    Offer.url slug or Offer.id.
  • visibility
    Dalam
    body
    Tipe
    string
    Diperlukan
    Opsional
    Deskripsi
    Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED.
  • Response: { offer: Offer }.
  • Fails if the listing is incomplete for publish.

API Stok

Lihat jumlah yang dapat dibeli per tingkatan, sesuaikan nama bidang pengiriman dengan daftar yang tepat, lalu isi ulang jumlah atau kunci dan akun yang disimpan.

Cara pencocokan bekerja

GET /api/v1/stock?fields=username,password menemukan daftar yang skemanya memiliki bidang tersebut. Isi ulang dengan nama opsi (atau optionId) dan nama bidang. Anda tidak perlu id bidang internal. Respons tidak pernah menyertakan nilai kredensial.

GET/api/v1/stock
offers:read

Daftar stok di seluruh daftar Anda

Mengembalikan jumlah per tingkatan dan nama bidang pengiriman sehingga Anda dapat mencocokkan kunci dan akun dengan penawaran yang tepat. Filter dengan q, fields, stockMode, dan lowStock. Tidak pernah mengembalikan nilai kredensial.

Permintaan

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

Ambil stok untuk satu daftar

Bentuk StockOffer yang sama seperti indeks, untuk satu url atau id numerik. Hanya jumlah.

Permintaan

  • urlOrId
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    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

Isi ulang daftar

Tingkatan jumlah: tambah, hapus, atau atur. Tingkatan item yang disimpan: objek item berdasarkan nama bidang, keys[] ketika ada satu bidang, atau teks terpisah. Beberapa tingkatan dalam satu panggilan melalui options[]. dryRun menampilkan pratinjau pencocokan. onDuplicate secara default dilewati.

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

Permintaan

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

Impor banyak akun atau kunci

Panduan

Gunakan POST /api/v1/offers/:url/stock dengan items[] untuk akun atau keys[] untuk kode lisensi satu bidang. Bagi menjadi 1.000 baris per permintaan.

  1. 1DAPATKAN listing. Gunakan fields[] dan stockMode untuk memilih item, kunci, atau menambah.
  2. 2Simpan akun sebagai JSON atau CSV yang dikunci berdasarkan nama field. Simpan kunci lisensi satu per baris.
  3. 3Jalankan uji coba terlebih dahulu. Periksa wouldImport, skippedDuplicates, dan matchedFields.
  4. 4Kirim ulang body yang sama tanpa dryRun untuk menulis stok.

Pilih payload yang sesuai dengan listing

Panggil GET stock terlebih dahulu. Jika fields[] memiliki lebih dari satu nama, kirim objek items yang dikunci dengan nama-nama tersebut (Username, Password, E-Mail). Jika hanya ada satu bidang, keys[] sudah cukup. Listing kuantitas menggunakan add, bukan items.

1.000 baris per permintaan. 5.000 item yang tidak terjual per tier. 300 permintaan per menit. Duplikat akan dilewati secara default.

accounts.json (satu objek per akun)

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

akun.csv

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

keys.txt (satu kunci lisensi per baris)

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]"}'

Impor batch TypeScript (1.000 baris per permintaan)

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

Apa yang dilindungi oleh API ini

Kunci memerlukan offers:write, dibatasi laju, dan hanya dapat mengisi ulang listing Anda sendiri. GET tidak pernah mengembalikan kredensial yang disimpan. Respons POST tidak mencerminkan Username, Password, atau nilai kunci. Kirim body melalui HTTPS di produksi dan simpan kunci API dalam variabel lingkungan.

Di Windows, gunakan curl.exe (bukan alias curl). Kutip -d JSON agar PowerShell tidak memisahkannya.

API Pesanan

Pesanan terikat pada akun penjual Anda. Detail penagihan pembeli mungkin disunting sesuai dengan aturan privasi marketplace-of-record.

GET/api/v1/orders
orders:read

Daftar pesanan penjual

Mendukung batas, offset, status, q, dan urutkan (terbaru, terlama, total_tinggi, total_rendah).

Permintaan

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

Dapatkan satu pesanan

Mengembalikan pesanan dengan item baris. Gunakan uid pesanan publik.

Permintaan

  • uid
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    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

Tandai terkirim

Pemenuhan manual. Baris COMPLEX harus sepenuhnya terlampir saat diperlukan. Mengeluarkan order.delivered.

Permintaan

  • uid
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    Order.uid.
  • evidence
    Dalam
    body
    Tipe
    string[]
    Diperlukan
    Opsional
    Batas
    HTTPS, max 10
    Deskripsi
    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.

Checkout yang Dihosting

Toko mitra yang disetujui atau backend mana pun dapat mengarahkan pembeli ke halaman pembayaran RMT.GG. Kami tetap sebagai merchant yang tercatat dan mengambil 4% dari jumlah yang terkunci.

Daftar putih dan pemenuhan

Terapkan di bawah Pengaturan, Checkout yang Dihosting, lalu buat kunci API dan webhook JSON di sana. Setelah pembayaran, kami mengeluarkan checkout.completed. Nilai pengiriman tetap di konfirmasi RMT.GG; mereka tidak ada di seller GET atau webhook.

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

Buat sesi checkout yang dihosting

Arahkan pembeli ke halaman pembayaran RMT.GG yang terkunci. Satu item: jumlah dan itemName. Keranjang: items[] dengan nama dan jumlah di setiap baris. Mata uang default adalah USD. Setelah pembayaran, pembeli tetap di RMT.GG ketika ada kolom pengiriman untuk disalin. returnUrl melanjutkan ke toko; tanpa pengiriman, kami mengirim mereka kembali setelah hitungan mundur singkat. Jumlah, panjang, dan batasan lainnya ada di kolom Batas.

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

Permintaan

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

Respon

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

Dapatkan sesi checkout yang dihosting

Mengembalikan sesi yang Anda buat. Gunakan ini jika checkout.completed tertunda. paid adalah true hanya ketika status sudah dibayar. items tidak pernah menyertakan nilai pengiriman.

Permintaan

  • uid
    Dalam
    path
    Tipe
    string
    Diperlukan
    diperlukan
    Deskripsi
    Session uid returned at create time.

Respon

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

Cari sesi checkout yang dihosting berdasarkan ID faktur

Objek sesi yang sama seperti GET berdasarkan uid. Kirim invoiceId yang Anda kirim saat membuat. Hilang: 400 INVOICE_ID_REQUIRED. Tidak diketahui: 404 NOT_FOUND.

Permintaan

  • invoiceId
    Dalam
    query
    Tipe
    string
    Diperlukan
    diperlukan
    Batas
    Max 128
    Deskripsi
    invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID.

Respon

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

Webhook keluar

Konfigurasikan endpoint HTTPS (atau webhook Discord) di Pengaturan → Pengembang. RMT POST saat peristiwa yang dilanggan terjadi.

order.paid
order.delivered
order.completed
order.refunded
order.disputed
offer.published
offer.updated
checkout.completed
checkout.canceled
checkout.refunded
  • Format JSON mengirimkan amplop terstruktur dengan id, tipe, dibuat, dan data.
  • Format Discord mengirimkan embed kaya dengan tautan pesanan atau penawaran.
  • Tanda tangan opsional menggunakan X-RMT-Timestamp dan X-RMT-Signature (skema yang sama seperti cadangan).
  • Riwayat pengiriman muncul di bawah setiap endpoint sehingga Anda dapat mencoba ulang kegagalan. Endpoint otomatis berhenti setelah kegagalan berulang.
  • Checkout yang dihosting mengirim checkout.completed, checkout.canceled, dan checkout.refunded dengan data.checkout. Nilai pengiriman dihilangkan. Penjualan marketplace menyimpan order.paid dan event order.* lainnya.

Amplop pengiriman JSON

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

Header pengiriman yang ditandatangani

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

payload checkout.selesai

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"
        }
      ]
    }
  }
}

payload checkout.canceled

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"
        }
      ]
    }
  }
}

Verifikasi tanda tangan webhook

Ketika rahasia tanda tangan diatur, hitung HMAC-SHA256 atas timestamp + '.' + rawBody dan bandingkan dengan hex setelah v1=.

Rahasia tetap di RMT. Setiap POST yang ditandatangani mencakup X-RMT-Timestamp (detik Unix) dan X-RMT-Signature (v1= plus hex). Hitung HMAC-SHA256 atas string timestamp + '.' + rawBody menggunakan rahasia Anda, lalu bandingkan dengan hex setelah v1=. Tolak timestamp yang lebih tua dari 5 menit.

  • Baca byte body mentah persis seperti yang diterima. Jangan parse JSON dan re-serialize sebelum hashing.
  • Gunakan nilai header X-RMT-Timestamp sebagai prefix timestamp (string yang sama, tidak diubah formatnya).
  • Bandingkan dengan pemeriksaan kesetaraan yang aman terhadap waktu. Tolak permintaan dengan tanda tangan yang hilang atau tidak cocok ketika rahasia telah dikonfigurasi.
  • Tolak timestamp yang lebih tua dari 5 menit untuk membatasi pengulangan. Skema yang sama berlaku untuk reserve.item dan acara pesanan keluar atau checkout.

Verifikasi TypeScript (perbandingan aman waktu dan jendela pengulangan 5 menit)

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

Pengelola webhook TypeScript

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

Cadangkan webhook (pengisian ulang inventaris)

Untuk daftar COMPLEX (unit unik), RMT dapat POST endpoint HTTPS Anda setelah pembayaran untuk mencetak lisensi, akun, atau kunci berikutnya ketika stok lokal kurang.

Kegagalan aman pembayaran

Jika endpoint Anda habis waktu atau mengembalikan data tidak valid, pesanan tetap PAID. Pembeli dikenakan biaya; Anda melihat kesalahan pada pesanan dan dapat mencoba ulang cadangan atau melampirkan kunci secara manual.

Cara mengaturnya

  1. Buat penawaran yang KOMPLEKS dengan bidang item (misalnya Lisensi).
  2. Pada langkah Item, aktifkan endpoint inventaris sesuai permintaan dan tempel URL HTTPS publik Anda.
  3. Secara opsional atur rahasia tanda tangan sehingga RMT mengirim X-RMT-Timestamp dan X-RMT-Signature di setiap panggilan.
  4. Jalankan Tes (atau tempel JSON contoh), peta jalur respons ke bidang item, lalu Simpan.
  5. Terbitkan listing. Pembeli dapat membeli dengan stok lokal kosong; kunci dibuat setelah pembayaran.
  • Stok lokal selalu diutamakan; webhook hanya mengisi kekurangan.
  • Konfigurasikan default tingkat penawaran, atau ganti per opsi harga, di langkah Item editor penawaran.
  • Hanya HTTPS. Tanda tangan HMAC opsional cocok dengan webhook keluar (X-RMT-Event: reserve.item).
  • Pengujian di editor mengirim dryRun: true. Di halaman pesanan, gunakan Retry reserve setelah memperbaiki endpoint Anda.

Badan POST kanonik (dipangkas)

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
}

Header permintaan (ketika rahasia tanda tangan diatur)

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

Respons kenyamanan

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

Bidang JSON yang dipetakan (dengan jalur responseMap seperti $.license)

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

Cara memverifikasi rahasia tanda tangan

Jika Anda mengatur rahasia pada penawaran, setiap POST reserve ditandatangani. Hitung ulang HMAC-SHA256(rahasia, timestamp + '.' + rawBody) dan bandingkan dengan X-RMT-Signature setelah menghapus prefix v1=. Rahasia itu sendiri tidak pernah disertakan dalam permintaan.

Lihat contoh verifikasi lengkap

Pengelola reserve TypeScript (verifikasi, lalu kembalikan entri)

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

Jangan panggil cadangan sebelum pembayaran

RMT hanya memanggil endpoint Anda setelah pembayaran berhasil, jadi checkout yang ditinggalkan tidak membakar lisensi.

Kesalahan dan batasan laju

Kesalahan mengembalikan JSON { error, code? }. Lalu lintas API Terbuka dibatasi pada 300 permintaan per menit per kunci API.

  • API_KEY_REQUIRED
    401

    Header Authorization atau X-Api-Key hilang.

  • API_KEY_INVALID
    401

    Kunci tidak dikenal, dicabut, kedaluwarsa, atau akses developer ditangguhkan.

  • SCOPE_MISSING
    403

    Kunci tidak memiliki lingkup yang diperlukan oleh endpoint.

  • RATE_LIMITED
    429

    Terlalu banyak permintaan. Hormati Retry-After dan X-RateLimit-Reset.

  • CHECKOUT_PARTNER_NOT_APPROVED
    403

    Penjual ini tidak disetujui untuk checkout yang dihosting.

  • INVALID_JSON
    400

    Badan permintaan harus berupa JSON.

  • INVALID_AMOUNT
    400

    jumlah harus lebih besar dari 0 dan paling banyak 1.000.000.

  • UNSUPPORTED_CURRENCY
    400

    mata uang bukan kode ISO yang didukung.

  • INVALID_RETURN_URL
    400

    returnUrl dan cancelUrl harus https (http://localhost diizinkan untuk toko lokal).

  • INVALID_PSP_CATEGORY
    400

    categorySlug dan offering harus dikirim bersama dan cocok dengan pasangan katalog.

  • INVALID_INVOICE_ID
    400

    invoiceId lebih dari 128 karakter.

  • INVOICE_ID_REQUIRED
    400

    GET /checkout/sessions memerlukan invoiceId sebagai parameter kueri.

  • INVALID_IDEMPOTENCY_KEY
    400

    Idempotency-Key lebih dari 128 karakter.

  • INVALID_METADATA
    400

    metadata harus berupa objek JSON, bukan array atau primitif.

  • METADATA_TOO_LARGE
    400

    Metadata yang diserialisasi lebih besar dari 4096 karakter.

  • IDEMPOTENCY_CONFLICT
    409

    Idempotency-Key digunakan kembali dengan jumlah, mata uang, atau item yang berbeda.

  • INVOICE_CONFLICT
    409

    invoiceId digunakan kembali dengan jumlah, mata uang, atau item yang berbeda.

  • INVALID_IMAGE_URL
    400

    imageUrl harus berupa URL https.

  • ITEM_NAME_REQUIRED
    400

    itemName (atau judul) diperlukan ketika items diabaikan.

  • INVALID_ITEMS
    400

    items harus berupa array tidak kosong dari item baris terkunci (maks 20). Setiap baris membutuhkan nama dan jumlah.

  • TOO_MANY_ITEMS
    400

    items tidak boleh mengandung lebih dari 20 baris.

  • AMOUNT_MISMATCH
    400

    jumlah harus sama dengan jumlah setiap baris dikali kuantitas.

  • INVALID_DELIVERY
    400

    bidang pengiriman tidak valid. Setiap bidang membutuhkan nama (maks 80) dan nilai (maks 2048). tipe harus berupa text, password, atau textarea (default text). Maks 16 bidang per baris.

  • ITEMS_TOO_LARGE
    400

    JSON item yang diserialisasi lebih besar dari 48.000 karakter.

  • NOT_FOUND
    404

    Tidak ada sesi checkout yang dihosting yang cocok dengan uid atau invoiceId untuk penjual ini.

  • RESERVE_FAILED
    400

    Webhook cadangan habis waktu, mengembalikan data tidak valid, atau melewatkan bidang yang diperlukan.

  • OPTION_AMBIGUOUS
    409

    Lebih dari satu opsi harga cocok dengan nama itu. Kirim optionId dari GET stock.

  • OPTION_NOT_FOUND
    404

    Tidak ada opsi harga yang cocok dengan id atau nama di daftar ini.

  • OPTION_REQUIRED
    400

    Daftar ini memiliki beberapa opsi harga. Kirim opsi atau optionId.

  • UNKNOWN_FIELD
    400

    Nama bidang tidak cocok dengan skema pengiriman daftar ini.

  • FIELD_MAPPING_AMBIGUOUS
    400

    Tidak dapat memetakan kolom atau kunci ke bidang pengiriman. Kirim header, atau gunakan objek item yang dikunci berdasarkan nama bidang.

  • STOCK_MODE_MISMATCH
    400

    Payload itu tidak cocok dengan mode stok opsi (jumlah vs item yang disimpan).

  • IMPORT_TOO_LARGE
    400

    Permintaan isi ulang dapat mengimpor paling banyak 1.000 item yang disimpan per opsi.

  • OPTION_ITEM_CAPACITY
    400

    Opsi harga ini sudah memiliki maksimum 5.000 item yang disimpan dan belum terjual.

  • DUPLICATE_ITEMS
    409

    onDuplicate=error dan setidaknya satu item sudah ada di opsi ini.

  • UNLIMITED_STOCK
    400

    Opsi ini memiliki jumlah tidak terbatas. Gunakan set untuk beralih ke jumlah terbatas terlebih dahulu.

  • INSUFFICIENT_STOCK
    400

    Jumlah stok tidak cukup untuk dihapus.

  • STOCK_HELD_IN_CHECKOUT
    400

    Tidak dapat mengurangi jumlah di bawah unit yang saat ini dipesan di checkout.

  • INVALID_RESTOCK
    400

    Badan isi ulang tidak memiliki aksi yang diperlukan, atau menggabungkan add/items dalam satu opsi.

Tangani 429

Kurangi penggunaan dengan Retry-After detik. Jangan putar kunci untuk menghindari batas; batasan berlaku per kunci dan datar untuk semua penjual.

Respons yang berhasil menyertakan X-RateLimit-Limit, X-RateLimit-Remaining, dan X-RateLimit-Reset.

Siap untuk mengotomatisasi?

Buat kunci di pengaturan Pengembang, dan sambungkan Discord atau Telegram di bawah Notifikasi.