/api/v1Dokumen penemuan
Mengembalikan lingkup, kuota, peristiwa webhook, dan katalog operasi lengkap. Setiap kunci API yang valid berfungsi.
API Terbuka Penjual ditujukan untuk penjual yang ingin mendapatkan pemberitahuan Discord, sinkronisasi stok, otomatisasi gaya Zapier, atau kantor belakang kustom di atas RMT.GG.
Buat kunci API di pengaturan Developer, lalu panggil discovery untuk mencetak katalog langsung.
/api/v1Mengembalikan lingkup, kuota, peristiwa webhook, dan katalog operasi lengkap. Setiap kunci API yang valid berfungsi.
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
Authorization: Bearer rmt_sk_live_<prefix>_<secret>Header alternatif
X-Api-Key: rmt_sk_live_<prefix>_<secret>Klien TypeScript yang dapat digunakan kembali (autentikasi Bearer, kesalahan terketik, coba ulang 429)
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.
Setiap kunci API membawa lingkup yang mengatur endpoint. Lingkup yang hilang mengembalikan 403 SCOPE_MISSING.
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).
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.
/api/v1/offersSaring dengan archive=active (default), diarsipkan, atau semua.
Permintaan
archive| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
archive | query | string | Opsional | One of "active" (default), "archived", or "all". |
/api/v1/offersMembuat draf kosong yang dimiliki oleh penjual yang terautentikasi. Tidak ada badan yang diperlukan.
/api/v1/offers/:urlOrIdMuat dengan slug URL publik atau id numerik. Hubungan (opsi) mungkin disertakan; item stok tidak.
Permintaan
urlOrId| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
urlOrId | path | string | diperlukan | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrIdPatch subset aman dari bidang daftar. Mengeluarkan offer.updated saat webhook keluar dikonfigurasi.
Permintaan
urlOrIdtitledescriptionvisibilitycategoryIdofferingIdthumbnailofferTypelistingMode| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
urlOrId | path | string | diperlukan | Offer.url slug or Offer.id. |
title | body | string | Opsional | Listing title. |
description | body | string | Opsional | Listing description. |
visibility | body | string | Opsional | PUBLIC | PRIVATE | UNPUBLISHED. |
categoryId | body | number | Opsional | Catalog category id. |
offeringId | body | number | Opsional | Catalog offering id. |
thumbnail | body | string | Opsional | Thumbnail URL or asset reference. |
offerType | body | string | Opsional | Offer type string used by the listing. |
listingMode | body | string | Opsional | Listing mode (for example STANDARD, RANK_BOOST, SESSION). |
/api/v1/offers/:urlOrIdAturan hapus/arsip yang sama seperti UI penjual.
Permintaan
urlOrId| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
urlOrId | path | string | diperlukan | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/publishMenerbitkan draf (atau mengubah visibilitas). Gagal dengan 400 jika bidang daftar yang diperlukan tidak lengkap.
Permintaan
urlOrIdvisibility| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
urlOrId | path | string | diperlukan | Offer.url slug or Offer.id. |
visibility | body | string | Opsional | Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED. |
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.
/api/v1/stockMengembalikan 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
qfieldsstockModelowStockarchive| Nama | Dalam | Tipe | Diperlukan | Batas | Deskripsi |
|---|---|---|---|---|---|
q | query | string | Opsional | Max 80 | Filter by listing title or url slug. |
fields | query | string | Opsional | Comma-separated delivery field names. The listing must have all of them (Username,Password). Names match case-insensitively. | |
stockMode | query | string | Opsional | QUANTITY or COMPLEX. Listing must have at least one option in that mode. | |
lowStock | query | number | Opsional | Keep listings that have a finite tier with available less than or equal to this number. | |
archive | query | string | Opsional | One of "active" (default), "archived", or "all". |
/api/v1/offers/:urlOrId/stockBentuk StockOffer yang sama seperti indeks, untuk satu url atau id numerik. Hanya jumlah.
Permintaan
urlOrId| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
urlOrId | path | string | diperlukan | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/stockTingkatan 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.
{
"option": "1 Month",
"add": 50
}Permintaan
urlOrIdoptionoptionIdaddremovesetitemskeystextdelimiterheadersoptionsdryRunonDuplicate| Nama | Dalam | Tipe | Diperlukan | Batas | Deskripsi |
|---|---|---|---|---|---|
urlOrId | path | string | diperlukan | Offer.url slug or Offer.id. | |
option | body | string | Kondisional | Pricing option name (case-insensitive). Omit when the listing has a single tier. | |
optionId | body | number | Kondisional | Pricing option id from GET stock. Wins over option when both are sent. Ambiguous names return 409 OPTION_AMBIGUOUS. | |
add | body | number | Kondisional | 1-1,000,000 | QUANTITY: add this many units. Fails with 400 UNLIMITED_STOCK if the tier is unlimited. |
remove | body | number | Kondisional | 1-1,000,000 | QUANTITY: withdraw this many units. Fails with 400 INSUFFICIENT_STOCK when there is not enough. |
set | body | number | null | Kondisional | QUANTITY: set an absolute count. null means unlimited. Cannot go below units held in checkout. | |
items | body | object[] | Kondisional | Max 1,000 | COMPLEX: objects keyed by delivery field name, for example { "Username": "a", "Password": "b" }. Names match case-insensitively. |
keys | body | string[] | Kondisional | Max 1,000 | COMPLEX: license keys when the listing has exactly one delivery field. Otherwise 400 FIELD_MAPPING_AMBIGUOUS. |
text | body | string | Kondisional | Max 1,000 rows | 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 | body | string | Opsional | Default : | Delimiter for text. Ignored unless text is sent. |
headers | body | string[] | Opsional | Optional column headers for text when the first line is data, not names. | |
options | body | object[] | Kondisional | Restock several tiers in one call. Each element is the same shape as a single-option body (option, add, items, …). | |
dryRun | body | boolean | Opsional | Preview matching and counts without writing. Default false. | |
onDuplicate | body | string | Opsional | skip (default) or error | COMPLEX: skip existing unsold fingerprints, or fail the request with 409 DUPLICATE_ITEMS. |
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.
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)
[
{ "Username": "player1", "Password": "secret1", "E-Mail": "[email protected]" },
{ "Username": "player2", "Password": "secret2", "E-Mail": "[email protected]" }
]akun.csv
Username,Password,E-Mail
player1,secret1,p1@example.com
player2,secret2,p2@example.comkeys.txt (satu kunci lisensi per baris)
AAAA-BBBB-CCCC
DDDD-EEEE-FFFF
GGGG-HHHH-IIIIcURL
# 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)
// 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.
Pesanan terikat pada akun penjual Anda. Detail penagihan pembeli mungkin disunting sesuai dengan aturan privasi marketplace-of-record.
/api/v1/ordersMendukung batas, offset, status, q, dan urutkan (terbaru, terlama, total_tinggi, total_rendah).
Permintaan
limitoffsetstatusqsort| Nama | Dalam | Tipe | Diperlukan | Batas | Deskripsi |
|---|---|---|---|---|---|
limit | query | number | Opsional | 1-100, default 20 | Page size. |
offset | query | number | Opsional | >= 0, default 0 | Skip this many rows. |
status | query | string | Opsional | Max 32 | Filter by order status (for example PAID, DELIVERED, COMPLETED). |
q | query | string | Opsional | Max 80 | Search reference or related text. |
sort | query | string | Opsional | newest (default) | newest | oldest | total_high | total_low. |
/api/v1/orders/:uidMengembalikan pesanan dengan item baris. Gunakan uid pesanan publik.
Permintaan
uid| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
uid | path | string | diperlukan | Order.uid. |
/api/v1/orders/:uid/deliverPemenuhan manual. Baris COMPLEX harus sepenuhnya terlampir saat diperlukan. Mengeluarkan order.delivered.
Permintaan
uidevidence| Nama | Dalam | Tipe | Diperlukan | Batas | Deskripsi |
|---|---|---|---|---|---|
uid | path | string | diperlukan | Order.uid. | |
evidence | body | string[] | Opsional | HTTPS, max 10 | Optional screenshot or transfer-proof URLs. |
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.
/api/v1/checkout/sessionsArahkan 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.
{
amount: 10, // what the buyer pays
itemName: "Gold pack", // pay page heading
}Permintaan
amountcurrencyitemNametitledescriptionimageUrlitemsitems[].nameitems[].titleitems[].descriptionitems[].amountitems[].quantityitems[].imageUrlitems[].deliveryitems[].delivery[].nameitems[].delivery[].typeitems[].delivery[].valueemailreturnUrlcancelUrlinvoiceIdcategorySlugofferingmetadataIdempotency-Key| Nama | Dalam | Tipe | Diperlukan | Batas | Deskripsi |
|---|---|---|---|---|---|
amount | body | number | Kondisional | > 0, max 1,000,000 | What the buyer pays. Required for a single item. With items[], omit it or send the line sum. Mismatch: 400 AMOUNT_MISMATCH. |
currency | body | string | Opsional | Default USD | ISO 4217 code such as USD or EUR. |
itemName | body | string | Kondisional | Max 120 | Pay page heading. Required for a single item. Alias: title. With items[], defaults to the first line name. Missing: 400 ITEM_NAME_REQUIRED. |
title | body | string | Opsional | Alias of itemName. If both are sent, itemName wins. | |
description | body | string | Opsional | Max 200 | Copy under the heading. If omitted, the heading is reused. |
imageUrl | body | string | Opsional | HTTPS, max 2048 | Product image, or fallback for lines without imageUrl. Invalid: 400 INVALID_IMAGE_URL. |
items | body | object[] | Kondisional | 1-20 lines, JSON max 48,000 | Locked cart. Required when amount is omitted. Buyers cannot change lines. Empty: 400 INVALID_ITEMS. |
items[].name | body | string | diperlukan | Max 120 | Line title. Alias: title. |
items[].title | body | string | Opsional | Alias of items[].name. If both are sent, name wins. | |
items[].description | body | string | Opsional | Max 200 | Line copy under the name. |
items[].amount | body | number | diperlukan | > 0, max 1,000,000 | Unit price. Session total is sum(amount * quantity). |
items[].quantity | body | number | Opsional | 1-99, default 1 | Locked on the pay page. |
items[].imageUrl | body | string | Opsional | HTTPS, max 2048 | Line image. Falls back to top-level imageUrl. |
items[].delivery | body | object[] | Opsional | Max 16 fields | Shown after payment on RMT.GG. Seller GET and webhooks omit values. |
items[].delivery[].name | body | string | diperlukan | Max 80 | Field label, for example Code or Password. |
items[].delivery[].type | body | string | Opsional | text, password, textarea | password is blurred until the buyer reveals it. Default text. |
items[].delivery[].value | body | string | diperlukan | Max 2048 | Field value. Numbers are stored as strings. Empty: 400 INVALID_DELIVERY. |
email | body | string | Opsional | Invalid values ignored | Prefills the pay page. The buyer still confirms email before paying. |
returnUrl | body | string | Opsional | HTTPS, max 2048 | 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 | body | string | Opsional | HTTPS, max 2048 | Redirect if the buyer cancels or the session expires. If omitted, they stay on the pay page. |
invoiceId | body | string | Opsional | Max 128 | Your shop id. Same payload returns the existing session. A different payload: 409 INVOICE_CONFLICT. |
categorySlug | body | string | Kondisional | With offering, or omit both | 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 | body | string | Kondisional | With categorySlug, or omit both | Catalog offering such as Mods. Mapped to labels like Games · Add-ons. |
metadata | body | object | Opsional | Object, max 4096 chars | Stored on the session. Not returned on seller GET. |
Idempotency-Key | header | string | Opsional | Max 128 | Replay header. Same key and payload returns the existing session. A different payload: 409 IDEMPOTENCY_CONFLICT. |
Respon
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditemshosted_urlexpires_at| Nama | Dalam | Tipe | Deskripsi |
|---|---|---|---|
uid | response | string | Session id. Same value as in hostedUrl / hosted_url. |
status | response | string | created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours. |
paid | response | boolean | true only when status is paid. false for refunded, expired, canceled, and unpaid states. |
amount | response | number | Locked buyer total in major units. |
currency | response | string | ISO currency code stored on the session (for example USD). |
itemName | response | string | null | Pay page heading. |
description | response | string | Longer copy under the heading. |
email | response | string | null | Prefill or confirmed buyer email. Guest checkout placeholders are returned as null. |
lang | response | string | null | Buyer locale when known. Not a create-session field. |
returnUrl | response | string | null | Continue-to-shop URL stored on the session, or null. |
cancelUrl | response | string | null | Cancel/expiry redirect, or null. |
invoiceId | response | string | null | Your invoice id. Same value as externalInvoiceId. |
externalInvoiceId | response | string | null | Same as invoiceId (legacy alias). |
source | response | string | How the session was created. API sessions are "api". |
expiresAt | response | string | ISO timestamp. Unpaid checkouts cannot be completed after this time. |
hostedUrl | response | string | Pay page URL (same target as top-level hosted_url on create). |
orderUid | response | string | null | Marketplace order uid after payment. null until the session is paid. |
items | response | object[] | Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values. |
hosted_url | response | string | Pay page URL. Send the buyer here. Same target as hostedUrl. |
expires_at | response | string | ISO timestamp. Same value as expiresAt. |
/api/v1/checkout/sessions/:uidMengembalikan 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| Nama | Dalam | Tipe | Diperlukan | Deskripsi |
|---|---|---|---|---|
uid | path | string | diperlukan | Session uid returned at create time. |
Respon
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nama | Dalam | Tipe | Deskripsi |
|---|---|---|---|
uid | response | string | Session id. Same value as in hostedUrl / hosted_url. |
status | response | string | created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours. |
paid | response | boolean | true only when status is paid. false for refunded, expired, canceled, and unpaid states. |
amount | response | number | Locked buyer total in major units. |
currency | response | string | ISO currency code stored on the session (for example USD). |
itemName | response | string | null | Pay page heading. |
description | response | string | Longer copy under the heading. |
email | response | string | null | Prefill or confirmed buyer email. Guest checkout placeholders are returned as null. |
lang | response | string | null | Buyer locale when known. Not a create-session field. |
returnUrl | response | string | null | Continue-to-shop URL stored on the session, or null. |
cancelUrl | response | string | null | Cancel/expiry redirect, or null. |
invoiceId | response | string | null | Your invoice id. Same value as externalInvoiceId. |
externalInvoiceId | response | string | null | Same as invoiceId (legacy alias). |
source | response | string | How the session was created. API sessions are "api". |
expiresAt | response | string | ISO timestamp. Unpaid checkouts cannot be completed after this time. |
hostedUrl | response | string | Pay page URL (same target as top-level hosted_url on create). |
orderUid | response | string | null | Marketplace order uid after payment. null until the session is paid. |
items | response | object[] | Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values. |
/api/v1/checkout/sessionsObjek 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| Nama | Dalam | Tipe | Diperlukan | Batas | Deskripsi |
|---|---|---|---|---|---|
invoiceId | query | string | diperlukan | Max 128 | invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID. |
Respon
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nama | Dalam | Tipe | Deskripsi |
|---|---|---|---|
uid | response | string | Session id. Same value as in hostedUrl / hosted_url. |
status | response | string | created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours. |
paid | response | boolean | true only when status is paid. false for refunded, expired, canceled, and unpaid states. |
amount | response | number | Locked buyer total in major units. |
currency | response | string | ISO currency code stored on the session (for example USD). |
itemName | response | string | null | Pay page heading. |
description | response | string | Longer copy under the heading. |
email | response | string | null | Prefill or confirmed buyer email. Guest checkout placeholders are returned as null. |
lang | response | string | null | Buyer locale when known. Not a create-session field. |
returnUrl | response | string | null | Continue-to-shop URL stored on the session, or null. |
cancelUrl | response | string | null | Cancel/expiry redirect, or null. |
invoiceId | response | string | null | Your invoice id. Same value as externalInvoiceId. |
externalInvoiceId | response | string | null | Same as invoiceId (legacy alias). |
source | response | string | How the session was created. API sessions are "api". |
expiresAt | response | string | ISO timestamp. Unpaid checkouts cannot be completed after this time. |
hostedUrl | response | string | Pay page URL (same target as top-level hosted_url on create). |
orderUid | response | string | null | Marketplace order uid after payment. null until the session is paid. |
items | response | object[] | Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values. |
Konfigurasikan endpoint HTTPS (atau webhook Discord) di Pengaturan → Pengembang. RMT POST saat peristiwa yang dilanggan terjadi.
Amplop pengiriman 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
{
"X-RMT-Event": "order.paid",
"X-RMT-Delivery": "whd_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}payload checkout.selesai
{
"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
{
"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"
}
]
}
}
}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.
Verifikasi TypeScript (perbandingan aman waktu dan jendela pengulangan 5 menit)
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
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;
}
}
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.
Badan POST kanonik (dipangkas)
{
"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)
{
"Content-Type": "application/json",
"X-RMT-Event": "reserve.item",
"X-RMT-Delivery": "rsv_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}Respons kenyamanan
{
"entries": [
{ "name": "License", "value": "AAAA-BBBB-CCCC" }
]
}Bidang JSON yang dipetakan (dengan jalur responseMap seperti $.license)
{
"license": "AAAA-BBBB-CCCC",
"email": "[email protected]",
"password": "temporary-pass"
}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 lengkapPengelola reserve TypeScript (verifikasi, lalu kembalikan entri)
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 mengembalikan JSON { error, code? }. Lalu lintas API Terbuka dibatasi pada 300 permintaan per menit per kunci API.
API_KEY_REQUIREDHeader Authorization atau X-Api-Key hilang.
API_KEY_INVALIDKunci tidak dikenal, dicabut, kedaluwarsa, atau akses developer ditangguhkan.
SCOPE_MISSINGKunci tidak memiliki lingkup yang diperlukan oleh endpoint.
RATE_LIMITEDTerlalu banyak permintaan. Hormati Retry-After dan X-RateLimit-Reset.
CHECKOUT_PARTNER_NOT_APPROVEDPenjual ini tidak disetujui untuk checkout yang dihosting.
INVALID_JSONBadan permintaan harus berupa JSON.
INVALID_AMOUNTjumlah harus lebih besar dari 0 dan paling banyak 1.000.000.
UNSUPPORTED_CURRENCYmata uang bukan kode ISO yang didukung.
INVALID_RETURN_URLreturnUrl dan cancelUrl harus https (http://localhost diizinkan untuk toko lokal).
INVALID_PSP_CATEGORYcategorySlug dan offering harus dikirim bersama dan cocok dengan pasangan katalog.
INVALID_INVOICE_IDinvoiceId lebih dari 128 karakter.
INVOICE_ID_REQUIREDGET /checkout/sessions memerlukan invoiceId sebagai parameter kueri.
INVALID_IDEMPOTENCY_KEYIdempotency-Key lebih dari 128 karakter.
INVALID_METADATAmetadata harus berupa objek JSON, bukan array atau primitif.
METADATA_TOO_LARGEMetadata yang diserialisasi lebih besar dari 4096 karakter.
IDEMPOTENCY_CONFLICTIdempotency-Key digunakan kembali dengan jumlah, mata uang, atau item yang berbeda.
INVOICE_CONFLICTinvoiceId digunakan kembali dengan jumlah, mata uang, atau item yang berbeda.
INVALID_IMAGE_URLimageUrl harus berupa URL https.
ITEM_NAME_REQUIREDitemName (atau judul) diperlukan ketika items diabaikan.
INVALID_ITEMSitems harus berupa array tidak kosong dari item baris terkunci (maks 20). Setiap baris membutuhkan nama dan jumlah.
TOO_MANY_ITEMSitems tidak boleh mengandung lebih dari 20 baris.
AMOUNT_MISMATCHjumlah harus sama dengan jumlah setiap baris dikali kuantitas.
INVALID_DELIVERYbidang 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_LARGEJSON item yang diserialisasi lebih besar dari 48.000 karakter.
NOT_FOUNDTidak ada sesi checkout yang dihosting yang cocok dengan uid atau invoiceId untuk penjual ini.
RESERVE_FAILEDWebhook cadangan habis waktu, mengembalikan data tidak valid, atau melewatkan bidang yang diperlukan.
OPTION_AMBIGUOUSLebih dari satu opsi harga cocok dengan nama itu. Kirim optionId dari GET stock.
OPTION_NOT_FOUNDTidak ada opsi harga yang cocok dengan id atau nama di daftar ini.
OPTION_REQUIREDDaftar ini memiliki beberapa opsi harga. Kirim opsi atau optionId.
UNKNOWN_FIELDNama bidang tidak cocok dengan skema pengiriman daftar ini.
FIELD_MAPPING_AMBIGUOUSTidak dapat memetakan kolom atau kunci ke bidang pengiriman. Kirim header, atau gunakan objek item yang dikunci berdasarkan nama bidang.
STOCK_MODE_MISMATCHPayload itu tidak cocok dengan mode stok opsi (jumlah vs item yang disimpan).
IMPORT_TOO_LARGEPermintaan isi ulang dapat mengimpor paling banyak 1.000 item yang disimpan per opsi.
OPTION_ITEM_CAPACITYOpsi harga ini sudah memiliki maksimum 5.000 item yang disimpan dan belum terjual.
DUPLICATE_ITEMSonDuplicate=error dan setidaknya satu item sudah ada di opsi ini.
UNLIMITED_STOCKOpsi ini memiliki jumlah tidak terbatas. Gunakan set untuk beralih ke jumlah terbatas terlebih dahulu.
INSUFFICIENT_STOCKJumlah stok tidak cukup untuk dihapus.
STOCK_HELD_IN_CHECKOUTTidak dapat mengurangi jumlah di bawah unit yang saat ini dipesan di checkout.
INVALID_RESTOCKBadan isi ulang tidak memiliki aksi yang diperlukan, atau menggabungkan add/items dalam satu opsi.
| Kode | HTTP | Deskripsi |
|---|---|---|
| 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.
Buat kunci di pengaturan Pengembang, dan sambungkan Discord atau Telegram di bawah Notifikasi.