/api/v1Document de découverte
Renvoie des scopes, des quotas, des événements de webhook et le catalogue complet des opérations. Toute clé API valide fonctionne.
L'API ouverte pour les vendeurs est destinée aux vendeurs qui souhaitent des alertes Discord, une synchronisation des stocks, une automatisation de style Zapier, ou un back office personnalisé sur RMT.GG.
Créez une clé API dans les paramètres Développeur, puis appelez la découverte pour imprimer le catalogue en direct.
/api/v1Renvoie des scopes, des quotas, des événements de webhook et le catalogue complet des opérations. Toute clé API valide fonctionne.
Envoyez votre clé secrète en direct à chaque requête /api/v1. Préférez uniquement HTTPS. Ne jamais intégrer de clés dans des clients publics ou des bundles de navigateur.
En-tête préféré
Authorization: Bearer rmt_sk_live_<prefix>_<secret>En-tête alternatif
X-Api-Key: rmt_sk_live_<prefix>_<secret>Client TypeScript réutilisable (auth Bearer, erreurs typées, réessai 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));
}
}
}
Faire tourner en cas de fuite
Si une clé fuit, révoquez-la dans les paramètres Développeur et créez-en une nouvelle. Mettez à jour votre automatisation avant de révoquer si vous êtes en direct.
Chaque clé API porte des scopes qui contrôlent les points de terminaison. Un scope manquant renvoie 403 SCOPE_MISSING.
offers:read: Lister et obtenir vos offres.offers:write: Créer, mettre à jour, publier et supprimer des offres.orders:read: Lister et obtenir les commandes des vendeurs.orders:write: Marquer les commandes comme livrées.webhooks:manage: Réservé à la gestion future des webhooks de l'Open API. Configurez Discord/Telegram dans les Notifications et les webhooks JSON dans les paramètres Développeur dès aujourd'hui.checkout:write: Créer et lire des sessions de paiement hébergées. Nécessite un paiement partenaire approuvé par l'administrateur.Scopes par défaut des clés
Les nouvelles clés reçoivent offers:read, offers:write, orders:read et orders:write. La gestion CRUD des webhooks sortants reste dans l'interface des paramètres (authentification de session).
Les identifiants d'offres acceptent le slug d'URL public ou l'ID numérique. Les réponses omettent l'ID interne et sellerId.
Ce que PATCH ne peut pas encore changer
Les lignes de stock, les prix des options, les médias et les attributs sont gérés dans l'éditeur de vendeur (ou futurs points de terminaison), pas via PATCH aujourd'hui.
/api/v1/offersFiltrer avec archive=active (par défaut), archivé ou tout.
Demande
archive| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
archive | query | string | Optionnel | One of "active" (default), "archived", or "all". |
/api/v1/offersCrée un brouillon vide appartenant au vendeur authentifié. Aucun corps requis.
/api/v1/offers/:urlOrIdCharger par slug d'URL public ou ID numérique. Les relations (options) peuvent être incluses ; les articles de stock ne le sont pas.
Demande
urlOrId| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
urlOrId | path | string | requis | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrIdPatch un sous-ensemble sécurisé des champs d'annonce. Émet offer.updated lorsque des webhooks sortants sont configurés.
Demande
urlOrIdtitledescriptionvisibilitycategoryIdofferingIdthumbnailofferTypelistingMode| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
urlOrId | path | string | requis | Offer.url slug or Offer.id. |
title | body | string | Optionnel | Listing title. |
description | body | string | Optionnel | Listing description. |
visibility | body | string | Optionnel | PUBLIC | PRIVATE | UNPUBLISHED. |
categoryId | body | number | Optionnel | Catalog category id. |
offeringId | body | number | Optionnel | Catalog offering id. |
thumbnail | body | string | Optionnel | Thumbnail URL or asset reference. |
offerType | body | string | Optionnel | Offer type string used by the listing. |
listingMode | body | string | Optionnel | Listing mode (for example STANDARD, RANK_BOOST, SESSION). |
/api/v1/offers/:urlOrIdLes mêmes règles de suppression/archivage que l'interface vendeur.
Demande
urlOrId| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
urlOrId | path | string | requis | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/publishPublie un brouillon (ou change la visibilité). Échoue avec 400 si les champs d'annonce requis sont incomplets.
Demande
urlOrIdvisibility| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
urlOrId | path | string | requis | Offer.url slug or Offer.id. |
visibility | body | string | Optionnel | Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED. |
Voir les quantités achetables par niveau, faire correspondre les noms des champs de livraison à la bonne annonce, puis réapprovisionner la quantité ou les clés et comptes sauvegardés.
Comment fonctionne la correspondance
GET /api/v1/stock?fields=username,password trouve des annonces dont le schéma a ces champs. Réapprovisionnez avec les noms d'options (ou optionId) et les noms de champs. Vous n'avez pas besoin d'identifiants de champ internes. Les réponses n'incluent jamais les valeurs d'identification.
/api/v1/stockRetourne les quantités par niveau et les noms des champs de livraison afin que vous puissiez faire correspondre les clés et comptes à la bonne offre. Filtrez avec q, fields, stockMode et lowStock. Ne retourne jamais les valeurs d'identification.
Demande
qfieldsstockModelowStockarchive| Nom | Dans | Type | Requis | Limites | Description |
|---|---|---|---|---|---|
q | query | string | Optionnel | Max 80 | Filter by listing title or url slug. |
fields | query | string | Optionnel | Comma-separated delivery field names. The listing must have all of them (Username,Password). Names match case-insensitively. | |
stockMode | query | string | Optionnel | QUANTITY or COMPLEX. Listing must have at least one option in that mode. | |
lowStock | query | number | Optionnel | Keep listings that have a finite tier with available less than or equal to this number. | |
archive | query | string | Optionnel | One of "active" (default), "archived", or "all". |
/api/v1/offers/:urlOrId/stockMême forme que StockOffer que l'index, pour une url ou un id numérique. Compte uniquement.
Demande
urlOrId| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
urlOrId | path | string | requis | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/stockNiveaux de quantité : ajouter, retirer ou définir. Niveaux d'articles sauvegardés : objets d'articles par nom de champ, keys[] lorsqu'il y a un champ, ou texte délimité. Plusieurs niveaux dans un seul appel via options[]. dryRun prévisualise la correspondance. onDuplicate par défaut à ignorer.
{
"option": "1 Month",
"add": 50
}Demande
urlOrIdoptionoptionIdaddremovesetitemskeystextdelimiterheadersoptionsdryRunonDuplicate| Nom | Dans | Type | Requis | Limites | Description |
|---|---|---|---|---|---|
urlOrId | path | string | requis | Offer.url slug or Offer.id. | |
option | body | string | Conditionnel | Pricing option name (case-insensitive). Omit when the listing has a single tier. | |
optionId | body | number | Conditionnel | Pricing option id from GET stock. Wins over option when both are sent. Ambiguous names return 409 OPTION_AMBIGUOUS. | |
add | body | number | Conditionnel | 1-1,000,000 | QUANTITY: add this many units. Fails with 400 UNLIMITED_STOCK if the tier is unlimited. |
remove | body | number | Conditionnel | 1-1,000,000 | QUANTITY: withdraw this many units. Fails with 400 INSUFFICIENT_STOCK when there is not enough. |
set | body | number | null | Conditionnel | QUANTITY: set an absolute count. null means unlimited. Cannot go below units held in checkout. | |
items | body | object[] | Conditionnel | Max 1,000 | COMPLEX: objects keyed by delivery field name, for example { "Username": "a", "Password": "b" }. Names match case-insensitively. |
keys | body | string[] | Conditionnel | Max 1,000 | COMPLEX: license keys when the listing has exactly one delivery field. Otherwise 400 FIELD_MAPPING_AMBIGUOUS. |
text | body | string | Conditionnel | 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 | Optionnel | Default : | Delimiter for text. Ignored unless text is sent. |
headers | body | string[] | Optionnel | Optional column headers for text when the first line is data, not names. | |
options | body | object[] | Conditionnel | Restock several tiers in one call. Each element is the same shape as a single-option body (option, add, items, …). | |
dryRun | body | boolean | Optionnel | Preview matching and counts without writing. Default false. | |
onDuplicate | body | string | Optionnel | skip (default) or error | COMPLEX: skip existing unsold fingerprints, or fail the request with 409 DUPLICATE_ITEMS. |
Utilisez POST /api/v1/offers/:url/stock avec items[] pour les comptes ou keys[] pour les codes de licence à champ unique. Limitez à 1 000 lignes par requête.
Choisissez la charge utile qui correspond à l'offre
Appelez d'abord GET stock. Si fields[] a plus d'un nom, envoyez des objets items indexés par ces noms (Nom d'utilisateur, Mot de passe, E-Mail). S'il y a exactement un champ, keys[] suffit. Les listes de quantité utilisent add, pas items.
1 000 lignes par demande. 5 000 objets non vendus par niveau. 300 demandes par minute. Les doublons sont ignorés par défaut.
accounts.json (un objet par compte)
[
{ "Username": "player1", "Password": "secret1", "E-Mail": "[email protected]" },
{ "Username": "player2", "Password": "secret2", "E-Mail": "[email protected]" }
]comptes.csv
Username,Password,E-Mail
player1,secret1,p1@example.com
player2,secret2,p2@example.comkeys.txt (une clé de licence par ligne)
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]"}'Importation par lot TypeScript (1 000 lignes par requête)
// 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) }),
}),
);
}
}
Ce que cette API protège
Les clés nécessitent offers:write, sont limitées en taux, et ne peuvent réapprovisionner que vos propres offres. GET ne renvoie jamais les identifiants enregistrés. Les réponses POST n'écho pas les valeurs de Nom d'utilisateur, Mot de passe ou clé. Envoyez le corps via HTTPS en production et gardez la clé API dans une variable d'environnement.
Sous Windows, utilisez curl.exe (pas l'alias curl). Entourez le -d JSON de guillemets pour que PowerShell ne le divise pas.
Les commandes sont limitées à votre compte vendeur. Les détails de facturation de l'acheteur peuvent être masqués selon les règles de confidentialité du marché.
/api/v1/ordersPrend en charge limit, offset, status, q et sort (nouveau, ancien, total_haut, total_bas).
Demande
limitoffsetstatusqsort| Nom | Dans | Type | Requis | Limites | Description |
|---|---|---|---|---|---|
limit | query | number | Optionnel | 1-100, default 20 | Page size. |
offset | query | number | Optionnel | >= 0, default 0 | Skip this many rows. |
status | query | string | Optionnel | Max 32 | Filter by order status (for example PAID, DELIVERED, COMPLETED). |
q | query | string | Optionnel | Max 80 | Search reference or related text. |
sort | query | string | Optionnel | newest (default) | newest | oldest | total_high | total_low. |
/api/v1/orders/:uidRenvoie la commande avec les lignes d'articles. Utilisez l'uid de commande public.
Demande
uid| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
uid | path | string | requis | Order.uid. |
/api/v1/orders/:uid/deliverExécution manuelle. Les lignes COMPLEX doivent être entièrement attachées lorsque requis. Émet order.delivered.
Demande
uidevidence| Nom | Dans | Type | Requis | Limites | Description |
|---|---|---|---|---|---|
uid | path | string | requis | Order.uid. | |
evidence | body | string[] | Optionnel | HTTPS, max 10 | Optional screenshot or transfer-proof URLs. |
Tout partenaire approuvé peut envoyer des acheteurs vers une page de paiement RMT.GG. Nous restons le marchand enregistré et prenons 4 % du montant bloqué.
Liste blanche et exécution
Appliquez sous Paramètres, Paiement hébergé, puis créez une clé API et un webhook JSON là-bas. Après le paiement, nous émettons checkout.completed. Les valeurs de livraison restent sur la confirmation RMT.GG ; elles ne sont pas dans le GET du vendeur ou les webhooks.
/api/v1/checkout/sessionsEnvoyez les acheteurs vers une page de paiement verrouillée de RMT.GG. Un article : montant et itemName. Panier : items[] avec nom et montant sur chaque ligne. La devise par défaut est l'USD. Après le paiement, l'acheteur reste sur RMT.GG lorsqu'il y a des champs de livraison à copier. returnUrl continue vers la boutique ; sans livraison, nous les renvoyons après un court compte à rebours. Montants, longueurs et autres limites se trouvent dans la colonne Limites.
{
amount: 10, // what the buyer pays
itemName: "Gold pack", // pay page heading
}Demande
amountcurrencyitemNametitledescriptionimageUrlitemsitems[].nameitems[].titleitems[].descriptionitems[].amountitems[].quantityitems[].imageUrlitems[].deliveryitems[].delivery[].nameitems[].delivery[].typeitems[].delivery[].valueemailreturnUrlcancelUrlinvoiceIdcategorySlugofferingmetadataIdempotency-Key| Nom | Dans | Type | Requis | Limites | Description |
|---|---|---|---|---|---|
amount | body | number | Conditionnel | > 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 | Optionnel | Default USD | ISO 4217 code such as USD or EUR. |
itemName | body | string | Conditionnel | 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 | Optionnel | Alias of itemName. If both are sent, itemName wins. | |
description | body | string | Optionnel | Max 200 | Copy under the heading. If omitted, the heading is reused. |
imageUrl | body | string | Optionnel | HTTPS, max 2048 | Product image, or fallback for lines without imageUrl. Invalid: 400 INVALID_IMAGE_URL. |
items | body | object[] | Conditionnel | 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 | requis | Max 120 | Line title. Alias: title. |
items[].title | body | string | Optionnel | Alias of items[].name. If both are sent, name wins. | |
items[].description | body | string | Optionnel | Max 200 | Line copy under the name. |
items[].amount | body | number | requis | > 0, max 1,000,000 | Unit price. Session total is sum(amount * quantity). |
items[].quantity | body | number | Optionnel | 1-99, default 1 | Locked on the pay page. |
items[].imageUrl | body | string | Optionnel | HTTPS, max 2048 | Line image. Falls back to top-level imageUrl. |
items[].delivery | body | object[] | Optionnel | Max 16 fields | Shown after payment on RMT.GG. Seller GET and webhooks omit values. |
items[].delivery[].name | body | string | requis | Max 80 | Field label, for example Code or Password. |
items[].delivery[].type | body | string | Optionnel | text, password, textarea | password is blurred until the buyer reveals it. Default text. |
items[].delivery[].value | body | string | requis | Max 2048 | Field value. Numbers are stored as strings. Empty: 400 INVALID_DELIVERY. |
email | body | string | Optionnel | Invalid values ignored | Prefills the pay page. The buyer still confirms email before paying. |
returnUrl | body | string | Optionnel | 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 | Optionnel | HTTPS, max 2048 | Redirect if the buyer cancels or the session expires. If omitted, they stay on the pay page. |
invoiceId | body | string | Optionnel | Max 128 | Your shop id. Same payload returns the existing session. A different payload: 409 INVOICE_CONFLICT. |
categorySlug | body | string | Conditionnel | 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 | Conditionnel | With categorySlug, or omit both | Catalog offering such as Mods. Mapped to labels like Games · Add-ons. |
metadata | body | object | Optionnel | Object, max 4096 chars | Stored on the session. Not returned on seller GET. |
Idempotency-Key | header | string | Optionnel | Max 128 | Replay header. Same key and payload returns the existing session. A different payload: 409 IDEMPOTENCY_CONFLICT. |
Réponse
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditemshosted_urlexpires_at| Nom | Dans | Type | Description |
|---|---|---|---|
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/:uidRenvoie la session que vous avez créée. Utilisez ceci si checkout.completed est retardé. paid est vrai uniquement lorsque le statut est payé. items n'inclut jamais les valeurs de livraison.
Demande
uid| Nom | Dans | Type | Requis | Description |
|---|---|---|---|---|
uid | path | string | requis | Session uid returned at create time. |
Réponse
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nom | Dans | Type | Description |
|---|---|---|---|
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/sessionsMême objet de session que GET par uid. Passez l'invoiceId que vous avez envoyé à la création. Manquant : 400 INVOICE_ID_REQUIRED. Inconnu : 404 NOT_FOUND.
Demande
invoiceId| Nom | Dans | Type | Requis | Limites | Description |
|---|---|---|---|---|---|
invoiceId | query | string | requis | Max 128 | invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID. |
Réponse
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nom | Dans | Type | Description |
|---|---|---|---|
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. |
Configurez des points de terminaison HTTPS (ou des webhooks Discord) dans Paramètres → Développeur. RMT POST lorsque les événements abonnés se déclenchent.
Enveloppe de livraison 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 */ ]
}
}
}En-têtes de livraison signés
{
"X-RMT-Event": "order.paid",
"X-RMT-Delivery": "whd_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}payload de commande terminée
{
"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"
}
]
}
}
}Lorsqu'un secret de signature est défini, calculez HMAC-SHA256 sur timestamp + '.' + rawBody et comparez avec l'hex après v1=.
Le secret reste sur RMT. Chaque POST signé inclut X-RMT-Timestamp (secondes Unix) et X-RMT-Signature (v1= plus hex). Calculez HMAC-SHA256 sur la chaîne timestamp + '.' + rawBody en utilisant votre secret, puis comparez avec l'hex après v1=. Rejetez les timestamps plus anciens que 5 minutes.
Vérification TypeScript (comparaison sécurisée par temps et fenêtre de répétition de 5 minutes)
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();
Gestionnaire de 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;
}
}
Pour les annonces COMPLEX (unité unique), RMT peut POST votre point de terminaison HTTPS après paiement pour créer la prochaine licence, compte ou clé lorsque le stock local est insuffisant.
Échecs sécurisés par paiement
Si votre point de terminaison expire ou renvoie des données invalides, la commande reste PAYÉE. L'acheteur est facturé ; vous voyez une erreur sur la commande et pouvez réessayer de réserver ou attacher des clés manuellement.
Corps POST canonique (tronqué)
{
"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
}En-têtes de requête (lorsqu'un secret de signature est défini)
{
"Content-Type": "application/json",
"X-RMT-Event": "reserve.item",
"X-RMT-Delivery": "rsv_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}Réponse de commodité
{
"entries": [
{ "name": "License", "value": "AAAA-BBBB-CCCC" }
]
}Champs JSON mappés (avec des chemins responseMap comme $.license)
{
"license": "AAAA-BBBB-CCCC",
"email": "[email protected]",
"password": "temporary-pass"
}Si vous avez défini un secret sur l'offre, chaque POST de réservation est signé. Recalculez HMAC-SHA256(secret, timestamp + '.' + rawBody) et comparez avec X-RMT-Signature après avoir supprimé le préfixe v1=. Le secret lui-même n'est jamais inclus dans la requête.
Voir l'exemple complet de vérificationGestionnaire de réservation TypeScript (vérifiez, puis retournez les entrées)
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 }],
});
}
Ne pas appeler la réserve avant le paiement
RMT n'appelle votre point de terminaison qu'après le succès du paiement, donc les abandons de panier ne consomment pas de licences.
Les erreurs renvoient JSON { error, code? }. Le trafic de l'API ouverte est limité à 300 requêtes par minute par clé API.
API_KEY_REQUIREDEn-tête Authorization ou X-Api-Key manquant.
API_KEY_INVALIDClé inconnue, révoquée, expirée ou accès développeur suspendu.
SCOPE_MISSINGLa clé manque le scope requis par le point de terminaison.
RATE_LIMITEDTrop de requêtes. Respectez Retry-After et X-RateLimit-Reset.
CHECKOUT_PARTNER_NOT_APPROVEDCe vendeur n'est pas approuvé pour le paiement hébergé.
INVALID_JSONLe corps de la requête doit être en JSON.
INVALID_AMOUNTle montant doit être supérieur à 0 et au maximum 1 000 000.
UNSUPPORTED_CURRENCYla devise n'est pas un code ISO pris en charge.
INVALID_RETURN_URLreturnUrl et cancelUrl doivent être en https (http://localhost est autorisé pour les boutiques locales).
INVALID_PSP_CATEGORYcategorySlug et offering doivent être envoyés ensemble et correspondre à une paire de catalogue.
INVALID_INVOICE_IDinvoiceId est plus long que 128 caractères.
INVOICE_ID_REQUIREDGET /checkout/sessions nécessite invoiceId comme paramètre de requête.
INVALID_IDEMPOTENCY_KEYIdempotency-Key est plus long que 128 caractères.
INVALID_METADATAmetadata doit être un objet JSON, pas un tableau ou une primitive.
METADATA_TOO_LARGELes métadonnées sérialisées sont plus grandes que 4096 caractères.
IDEMPOTENCY_CONFLICTIdempotency-Key a été réutilisé avec un montant, une devise ou un article différent.
INVOICE_CONFLICTinvoiceId a été réutilisé avec un montant, une devise ou un article différent.
INVALID_IMAGE_URLimageUrl doit être une URL https.
ITEM_NAME_REQUIREDitemName (ou titre) est requis lorsque les items sont omis.
INVALID_ITEMSitems doit être un tableau non vide d'articles verrouillés (max 20). Chaque ligne a besoin d'un nom et d'un montant.
TOO_MANY_ITEMSitems ne peut pas contenir plus de 20 lignes.
AMOUNT_MISMATCHle montant doit être égal à la somme de chaque montant de ligne multiplié par la quantité.
INVALID_DELIVERYLes champs de livraison sont invalides. Chaque champ a besoin d'un nom (max 80) et d'une valeur (max 2048). Le type doit être texte, mot de passe ou zone de texte (texte par défaut). Max 16 champs par ligne.
ITEMS_TOO_LARGELes éléments JSON sérialisés sont plus grands que 48 000 caractères.
NOT_FOUNDAucune session de paiement hébergé ne correspond à cet uid ou invoiceId pour ce vendeur.
RESERVE_FAILEDLe webhook de réservation a expiré, renvoyé des données invalides ou manqué des champs requis.
OPTION_AMBIGUOUSPlus d'une option de tarification correspond à ce nom. Passez optionId depuis GET stock.
OPTION_NOT_FOUNDAucune option de tarification ne correspond à cet id ou nom sur cette annonce.
OPTION_REQUIREDCette annonce a plusieurs options de tarification. Passez option ou optionId.
UNKNOWN_FIELDUn nom de champ ne correspond pas au schéma de livraison de cette annonce.
FIELD_MAPPING_AMBIGUOUSImpossible de mapper les colonnes ou clés aux champs de livraison. Envoyez des en-têtes, ou utilisez des objets d'articles indexés par nom de champ.
STOCK_MODE_MISMATCHCette charge utile ne correspond pas au mode de stock de l'option (quantité vs articles sauvegardés).
IMPORT_TOO_LARGEUne demande de réapprovisionnement peut importer au maximum 1 000 articles sauvegardés par option.
OPTION_ITEM_CAPACITYCette option de tarification a déjà le maximum de 5 000 articles sauvegardés non vendus.
DUPLICATE_ITEMSonDuplicate=error et au moins un article existe déjà sur cette option.
UNLIMITED_STOCKCette option a une quantité illimitée. Utilisez set pour passer d'abord à un compte fini.
INSUFFICIENT_STOCKPas assez de stock de quantité à retirer.
STOCK_HELD_IN_CHECKOUTImpossible de réduire la quantité en dessous des unités actuellement réservées dans le panier.
INVALID_RESTOCKLe corps de réapprovisionnement manque d'une action requise, ou combine add/items dans une option.
| Code | HTTP | Description |
|---|---|---|
| API_KEY_REQUIRED | 401 | En-tête Authorization ou X-Api-Key manquant. |
| API_KEY_INVALID | 401 | Clé inconnue, révoquée, expirée ou accès développeur suspendu. |
| SCOPE_MISSING | 403 | La clé manque le scope requis par le point de terminaison. |
| RATE_LIMITED | 429 | Trop de requêtes. Respectez Retry-After et X-RateLimit-Reset. |
| CHECKOUT_PARTNER_NOT_APPROVED | 403 | Ce vendeur n'est pas approuvé pour le paiement hébergé. |
| INVALID_JSON | 400 | Le corps de la requête doit être en JSON. |
| INVALID_AMOUNT | 400 | le montant doit être supérieur à 0 et au maximum 1 000 000. |
| UNSUPPORTED_CURRENCY | 400 | la devise n'est pas un code ISO pris en charge. |
| INVALID_RETURN_URL | 400 | returnUrl et cancelUrl doivent être en https (http://localhost est autorisé pour les boutiques locales). |
| INVALID_PSP_CATEGORY | 400 | categorySlug et offering doivent être envoyés ensemble et correspondre à une paire de catalogue. |
| INVALID_INVOICE_ID | 400 | invoiceId est plus long que 128 caractères. |
| INVOICE_ID_REQUIRED | 400 | GET /checkout/sessions nécessite invoiceId comme paramètre de requête. |
| INVALID_IDEMPOTENCY_KEY | 400 | Idempotency-Key est plus long que 128 caractères. |
| INVALID_METADATA | 400 | metadata doit être un objet JSON, pas un tableau ou une primitive. |
| METADATA_TOO_LARGE | 400 | Les métadonnées sérialisées sont plus grandes que 4096 caractères. |
| IDEMPOTENCY_CONFLICT | 409 | Idempotency-Key a été réutilisé avec un montant, une devise ou un article différent. |
| INVOICE_CONFLICT | 409 | invoiceId a été réutilisé avec un montant, une devise ou un article différent. |
| INVALID_IMAGE_URL | 400 | imageUrl doit être une URL https. |
| ITEM_NAME_REQUIRED | 400 | itemName (ou titre) est requis lorsque les items sont omis. |
| INVALID_ITEMS | 400 | items doit être un tableau non vide d'articles verrouillés (max 20). Chaque ligne a besoin d'un nom et d'un montant. |
| TOO_MANY_ITEMS | 400 | items ne peut pas contenir plus de 20 lignes. |
| AMOUNT_MISMATCH | 400 | le montant doit être égal à la somme de chaque montant de ligne multiplié par la quantité. |
| INVALID_DELIVERY | 400 | Les champs de livraison sont invalides. Chaque champ a besoin d'un nom (max 80) et d'une valeur (max 2048). Le type doit être texte, mot de passe ou zone de texte (texte par défaut). Max 16 champs par ligne. |
| ITEMS_TOO_LARGE | 400 | Les éléments JSON sérialisés sont plus grands que 48 000 caractères. |
| NOT_FOUND | 404 | Aucune session de paiement hébergé ne correspond à cet uid ou invoiceId pour ce vendeur. |
| RESERVE_FAILED | 400 | Le webhook de réservation a expiré, renvoyé des données invalides ou manqué des champs requis. |
| OPTION_AMBIGUOUS | 409 | Plus d'une option de tarification correspond à ce nom. Passez optionId depuis GET stock. |
| OPTION_NOT_FOUND | 404 | Aucune option de tarification ne correspond à cet id ou nom sur cette annonce. |
| OPTION_REQUIRED | 400 | Cette annonce a plusieurs options de tarification. Passez option ou optionId. |
| UNKNOWN_FIELD | 400 | Un nom de champ ne correspond pas au schéma de livraison de cette annonce. |
| FIELD_MAPPING_AMBIGUOUS | 400 | Impossible de mapper les colonnes ou clés aux champs de livraison. Envoyez des en-têtes, ou utilisez des objets d'articles indexés par nom de champ. |
| STOCK_MODE_MISMATCH | 400 | Cette charge utile ne correspond pas au mode de stock de l'option (quantité vs articles sauvegardés). |
| IMPORT_TOO_LARGE | 400 | Une demande de réapprovisionnement peut importer au maximum 1 000 articles sauvegardés par option. |
| OPTION_ITEM_CAPACITY | 400 | Cette option de tarification a déjà le maximum de 5 000 articles sauvegardés non vendus. |
| DUPLICATE_ITEMS | 409 | onDuplicate=error et au moins un article existe déjà sur cette option. |
| UNLIMITED_STOCK | 400 | Cette option a une quantité illimitée. Utilisez set pour passer d'abord à un compte fini. |
| INSUFFICIENT_STOCK | 400 | Pas assez de stock de quantité à retirer. |
| STOCK_HELD_IN_CHECKOUT | 400 | Impossible de réduire la quantité en dessous des unités actuellement réservées dans le panier. |
| INVALID_RESTOCK | 400 | Le corps de réapprovisionnement manque d'une action requise, ou combine add/items dans une option. |
Gérer 429
Ralentissez en utilisant Retry-After secondes. Ne faites pas tourner les clés pour contourner les limites ; la limite est par clé et uniforme pour tous les vendeurs.
Les réponses réussies incluent X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset.
Créez une clé dans les paramètres Développeur et connectez Discord ou Telegram sous Notifications.