API REST - v1
API de recharge de jeux et de cartes cadeaux pour revendeurs
Une seule API REST pour les recharges de jeux et les codes de cartes cadeaux : lisez le catalogue, validez un joueur, créez une commande et récupérez le résultat par webhook ou par interrogation du statut.
Il s'agit de l'API de recharge host-to-host (H2H) qui alimente SHOP2TOPUP. C'est une API JSON classique sur HTTPS, avec authentification Bearer, clĂ©s d'idempotence UUID, limites de dĂ©bit publiĂ©es et codes d'erreur stables lisibles par une machine : une boutique, un site ou un bot peut donc vendre du crĂ©dit de jeu par programmation, sans bibliothĂšque cliente. La mĂȘme interface a portĂ© 3,000,000+ commandes livrĂ©es sur 5+ annĂ©es d'exploitation continue.
Vous cherchez les tarifs, les moyens de paiement et le fonctionnement du compte partenaire ? Lisez la présentation du programme revendeur SHOP2TOPUP.
Votre premier appel, avant mĂȘme de signer quoi que ce soit
Chaque endpoint est une simple requĂȘte HTTPS portant un seul en-tĂȘte. Aucun SDK Ă installer, aucune bibliothĂšque cliente Ă embarquer, aucune Ă©tape de build Ă ajouter. Collez l'appel ci-dessous, insĂ©rez votre clĂ©, et vous obtenez une rĂ©ponse du catalogue en direct dans votre terminal.
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
-H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"{
"success": true,
"categories": [
{
"id": 12,
"name": "Free Fire",
"description": "Garena Free Fire diamonds",
"big_category_id": 1,
"big_category_name": "Mobile Games"
}
]
}URL de base
https://shop2topup.com/api/endpoints/v1
Tous les chemins documentés sont relatifs à cette base. Les réponses sont en JSON et portent toujours un champ booléen success : une seule condition dans votre client sépare donc le cas nominal de tout le reste.
Enveloppe d'erreur
Les Ă©checs conservent la mĂȘme forme quel que soit le code de statut. Branchez-vous sur error.code, qui fait partie du contrat ; jamais sur error.message, Ă©crit pour des humains et susceptible d'ĂȘtre reformulĂ© Ă tout moment.
{
"success": false,
"error": {
"code": "PRICE_INCREASED",
"message": "Price has increased beyond expected",
"details": {
"expected_unit_price": "0.950000",
"current_unit_price": "0.980000"
}
}
}Toute la surface de l'API sur un seul écran
Onze endpoints couvrent l'intĂ©gration complĂšte : lire le catalogue, tarifer un article, vĂ©rifier un joueur, passer une commande et la rapprocher ensuite. Chaque ligne renvoie directement Ă la rĂ©fĂ©rence complĂšte, avec tous les paramĂštres, un exemple de requĂȘte et un exemple de rĂ©ponse.
| Méthode | Chemin | RÎle | Référence |
|---|---|---|---|
| GET | /account | Get Account Info | Lire la référence |
| GET | /catalog/big-categories | List Big Categories | Lire la référence |
| GET | /catalog/categories | List Categories | Lire la référence |
| GET | /catalog/subcategories | List Subcategories (Products) | Lire la référence |
| GET | /catalog/subcategory/:itemId/price | Get Item Price | Lire la référence |
| GET | /catalog/category/:categoryId/requirements | Get Category Requirements | Lire la référence |
| POST | /player/validate | Validate Player | Lire la référence |
| POST | /orders/create | Create Order | Lire la référence |
| GET | /orders/:orderId | Get Order Status | Lire la référence |
| POST | /orders/batch | Batch Get Orders | Lire la référence |
| GET | /orders | List Orders | Lire la référence |
S'y ajoutent les webhooks de statut de commande, documentés en détail plus bas sur cette page.
Authentification et contrĂŽle d'accĂšs
L'authentification tient dans un seul en-tĂȘte HTTP. Chaque requĂȘte porte un en-tĂȘte Authorization contenant un identifiant Bearer composĂ© d'un identifiant de clĂ© et d'un secret, dĂ©livrĂ©s depuis la page API Access de votre panneau revendeur. L'identifiant de clĂ© dĂ©signe le compte ; le secret prouve qu'il vous appartient et est vĂ©rifiĂ© cĂŽtĂ© serveur par un contrĂŽle HMAC.
Le secret n'est affiché qu'une fois, au moment de sa création, et n'est jamais récupérable ensuite. Placez-le dans le gestionnaire de secrets que votre stack utilise déjà , injectez-le comme variable d'environnement, et gardez-le hors du dépÎt de code. Si vous le perdez, vous faites une rotation plutÎt qu'une récupération.
Il n'y a pas de redirection OAuth, pas de cookie de session, pas de refresh token et rien Ă planifier. C'est important pour les intĂ©grations automatisĂ©es : une tĂąche cron ou un worker de file d'attente peut conserver le mĂȘme identifiant pendant des annĂ©es sans procĂ©dure de renouvellement, et aucune horloge d'expiration ne vous rĂ©veillera Ă trois heures du matin.
Une liste d'IP autorisĂ©es peut ĂȘtre attachĂ©e Ă une clĂ©. DĂšs qu'elle n'est plus vide, les requĂȘtes provenant de toute autre adresse sont rejetĂ©es avec IP_NOT_ALLOWED et un HTTP 403, avant l'exĂ©cution de la moindre logique mĂ©tier : un identifiant fuitĂ© est donc inutilisable hors de vos propres serveurs. La rotation est un dĂ©ploiement, pas une migration : crĂ©ez la nouvelle clĂ©, livrez-la, supprimez l'ancienne.
Les clés appartiennent à un seul compte revendeur et dépensent le portefeuille de ce compte : n'envoyez donc jamais un secret vers un navigateur, un bundle mobile ou un dépÎt public. Appelez l'API depuis votre backend et laissez votre propre front-end dialoguer avec ce backend. Tout ce qui est documenté ici suppose un appelant serveur à serveur.
Authorization: Bearer <keyId>.<secret>Vérifier une clé en un seul appel
GET /account est le contrĂŽle de santĂ© d'un identifiant. Il rĂ©pond Ă trois questions Ă la fois : la clĂ© s'authentifie-t-elle, le compte est-il actif, et le portefeuille couvre-t-il ce que vous vous apprĂȘtez Ă commander. ExĂ©cutez-le au dĂ©marrage et avant tout traitement par lots.
Lire la référence authentification et compteUne intégration complÚte, de bout en bout
Une intégration fonctionnelle tient en quatre étapes. Les exemples ci-dessous les exécutent toutes sur l'API réelle, en cURL, Node et PHP. Remplacez l'identifiant par le vÎtre et ils s'exécutent tels quels.
- 1
Lire le catalogue
Parcourez les grandes catĂ©gories, puis les catĂ©gories, puis les sous-catĂ©gories, et lisez le prix actuel de l'article que vous ĂȘtes sur le point de vendre. Mettez l'arborescence en cache ; tarifez l'article Ă la volĂ©e.
- 2
Valider le joueur
Résolvez l'identifiant en un nom in-game et une région avant tout mouvement d'argent. C'est là que se rattrapent les fautes de frappe et les erreurs de région.
- 3
Créer la commande
GĂ©nĂ©rez un UUID, persistez-le, puis envoyez-le comme order_id. C'est votre clĂ© d'idempotence, et c'est la seule façon sĂ»re de relancer une requĂȘte de crĂ©ation.
- 4
Confirmer la livraison
Attendez le webhook de statut de commande, et repliez-vous sur une lecture du statut pour tout ce qui reste pending au-delà de votre propre délai.
cURL - flux completAfficher le codeMasquer le code
export S2T_KEY="YOUR_KEY_ID.YOUR_KEY_SECRET"
export S2T_BASE="https://shop2topup.com/api/endpoints/v1"
# 1. Find the item you want to sell.
curl -s "$S2T_BASE/catalog/subcategories?categoryId=12" \
-H "Authorization: Bearer $S2T_KEY"
# 2. Read the exact price you will be charged for it.
curl -s "$S2T_BASE/catalog/subcategory/999/price" \
-H "Authorization: Bearer $S2T_KEY"
# 3. Confirm the player exists BEFORE any money moves.
curl -s -X POST "$S2T_BASE/player/validate" \
-H "Authorization: Bearer $S2T_KEY" \
-H "Content-Type: application/json" \
-d '{"sub_category_id": 999, "player_id": "123456789", "server": "Asia"}'
# 4. Create the order. order_id is YOUR uuid and YOUR idempotency key.
curl -s -X POST "$S2T_BASE/orders/create" \
-H "Authorization: Bearer $S2T_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "01912345-6789-7abc-8def-0123456789ab",
"sub_category_id": 999,
"quantity": 1,
"requirements": { "player_id": "123456789", "server": "Asia" },
"expected_unit_price": "0.950000"
}'
# 5. Read the order back until it leaves "pending".
curl -s "$S2T_BASE/orders/01912345-6789-7abc-8def-0123456789ab" \
-H "Authorization: Bearer $S2T_KEY"Node.js - flux completAfficher le codeMasquer le code
// Node 18+ â no dependencies, global fetch and global crypto.
const BASE = 'https://shop2topup.com/api/endpoints/v1';
const KEY = process.env.S2T_KEY; // "<keyId>.<secret>", server-side only.
async function call(path, init = {}) {
const res = await fetch(BASE + path, {
...init,
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
...(init.headers || {}),
},
});
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') || 5);
await new Promise((r) => setTimeout(r, wait * 1000));
return call(path, init);
}
const body = await res.json();
if (!res.ok || body.success === false) {
throw Object.assign(new Error(body?.error?.code || 'HTTP_' + res.status), {
code: body?.error?.code,
status: res.status,
});
}
return body;
}
async function sell({ itemId, categoryId, playerId, server }) {
// 1 + 2. Catalog and current price.
const { subcategories } = await call(
`/catalog/subcategories?categoryId=${categoryId}`,
);
const item = subcategories.find((s) => s.item_id === itemId);
const { price } = await call(`/catalog/subcategory/${itemId}/price`);
// 3. Player check before charging.
const { player } = await call('/player/validate', {
method: 'POST',
body: JSON.stringify({
sub_category_id: itemId,
player_id: playerId,
server,
}),
});
// 4. Create the order. Persist orderId BEFORE the call so a crash mid-flight
// can be replayed with the same uuid instead of double-charging.
const orderId = crypto.randomUUID();
await saveIntent(orderId, itemId, playerId);
const { order } = await call('/orders/create', {
method: 'POST',
body: JSON.stringify({
order_id: orderId,
sub_category_id: itemId,
quantity: 1,
requirements: { player_id: playerId, server },
expected_unit_price: price.unit_price,
}),
});
// 5. Confirm. A webhook usually beats the poll; the poll is the safety net.
let status = order.status;
while (status === 'pending') {
await new Promise((r) => setTimeout(r, 20000));
status = (await call(`/orders/${orderId}`)).order.status;
}
return { orderId, status, itemName: item?.name, playerName: player.player_name };
}PHP - flux completAfficher le codeMasquer le code
<?php
// PHP 8 â plain cURL, no SDK required.
const S2T_BASE = 'https://shop2topup.com/api/endpoints/v1';
function s2t(string $path, ?array $body = null): array
{
$ch = curl_init(S2T_BASE . $path);
$headers = [
'Authorization: Bearer ' . getenv('S2T_KEY'),
'Content-Type: application/json',
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true) ?: [];
if ($status >= 400 || ($data['success'] ?? false) === false) {
throw new RuntimeException($data['error']['code'] ?? 'HTTP_' . $status);
}
return $data;
}
// 1 + 2. Catalog, then the exact price for the item.
$catalog = s2t('/catalog/subcategories?categoryId=12');
$price = s2t('/catalog/subcategory/999/price');
// 3. Player check before charging.
$player = s2t('/player/validate', [
'sub_category_id' => 999,
'player_id' => '123456789',
'server' => 'Asia',
]);
// 4. Create the order with your own uuid as the idempotency key.
$orderId = sprintf(
'%04x%04x-%04x-7%03x-%04x-%04x%04x%04x',
random_int(0, 0xffff), random_int(0, 0xffff),
random_int(0, 0xffff), random_int(0, 0x0fff),
random_int(0x8000, 0xbfff),
random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
);
$order = s2t('/orders/create', [
'order_id' => $orderId,
'sub_category_id' => 999,
'quantity' => 1,
'requirements' => ['player_id' => '123456789', 'server' => 'Asia'],
'expected_unit_price' => $price['price']['unit_price'],
]);
// 5. Read it back until it settles.
do {
sleep(20);
$latest = s2t('/orders/' . $orderId);
} while ($latest['order']['status'] === 'pending');
echo $latest['order']['status'];Deux dĂ©tails mĂ©ritent d'ĂȘtre copiĂ©s Ă l'identique : persistez l'UUID de commande avant l'appel de crĂ©ation, pas aprĂšs, et relisez le prix plutĂŽt que de faire confiance Ă une valeur en cache. Ces deux habitudes Ă©liminent presque tous les doubles dĂ©bits et tous les rejets PRICE_INCREASED.
Webhooks : rappels de statut de commande
Enregistrez un endpoint HTTPS et la plateforme y pousse les résultats des commandes au fil de l'eau : votre boutique cesse d'interroger et se met à réagir. Le rappel est signé, vous pouvez donc prouver que la charge utile vient bien de nous avant d'agir dessus.
ĂvĂ©nements
| ĂvĂ©nement | DĂ©clenchĂ© quand |
|---|---|
| order.completed | The order finished and everything it owed the buyer was delivered. |
| order.failed | The order failed and no sub-transaction is left pending or processing. |
| order.refunded | The order was fully refunded back to your wallet. |
| webhook.test | You triggered a signed test delivery from the panel to check your handler. |
En-tĂȘtes de livraison
| En-tĂȘte | Valeur |
|---|---|
| Content-Type | application/json |
| X-Shop2Topup-Signature | sha256=<hex> â HMAC-SHA256 of the raw request body |
| X-Shop2Topup-Event | Event name, e.g. order.completed |
| User-Agent | shop2topup-webhook/1.0 |
Charge utile du rappel
Chaque rappel porte les mĂȘmes trois champs de premier niveau â event, timestamp et data â et data reproduit l'objet commande renvoyĂ© par l'endpoint de statut. Les codes de cartes cadeaux ne sont prĂ©sents que sur une commande completed, player_name peut ĂȘtre null, et sub_transaction_summary apparaĂźt chaque fois que la commande a Ă©tĂ© dĂ©coupĂ©e en unitĂ©s.
{
"event": "order.completed",
"timestamp": "2026-03-11T14:30:00.000Z",
"data": {
"order_id": "01912345-6789-7abc-8def-0123456789ab",
"status": "completed",
"player_id": "123456789",
"player_name": "ProGamer99",
"subcategory_name": "FF 100 Diamonds",
"quantity": 1,
"charged_amount": "0.950000",
"currency": "USD",
"created_at": "2026-03-11T14:28:00.000Z",
"completed_at": "2026-03-11T14:30:00.000Z",
"vouchers": [
{
"code": "XXXX-YYYY-ZZZZ",
"serial_number": "SN-9928371",
"expiry_date": "2027-03-11"
}
],
"sub_transaction_summary": {
"total": 1,
"completed": 1,
"failed": 0,
"pending": 0,
"processing": 0,
"retrying": 0,
"refunded": 0
}
}
}Vérification de la signature
Chaque livraison porte X-Shop2Topup-Signature, formĂ© de sha256= suivi du HMAC-SHA256 hexadĂ©cimal du corps brut de la requĂȘte sous votre secret de webhook. VĂ©rifiez-le sur les octets bruts, avant tout parsing JSON, et comparez en temps constant. Un handler qui parse d'abord et vĂ©rifie ensuite peut ĂȘtre trompĂ© par un corps rĂ©-sĂ©rialisĂ©.
// Express â verify the signature against the RAW body, not the parsed object.
const crypto = require('crypto');
app.post(
'/shop2topup/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.get('X-Shop2Topup-Signature') || '';
const expected =
'sha256=' +
crypto
.createHmac('sha256', process.env.S2T_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end();
}
const payload = JSON.parse(req.body.toString('utf8'));
// Answer fast, then do the work. The delivery times out after 10 seconds.
res.status(200).end();
enqueueSettlement(payload.data.order_id, payload.event);
},
);Livraison et relances
Un rappel est tenté une seule fois et votre endpoint dispose de dix secondes pour répondre. Il n'y a ni relance automatique ni backoff exponentiel derriÚre, et c'est délibéré : l'endpoint de statut fait foi, donc le rapprochement relÚve d'une interrogation que vous maßtrisez plutÎt que d'un calendrier de relances invisible. Répondez 200 immédiatement et faites le travail sur votre propre file.
Handlers idempotents
Indexez votre handler sur order_id et faites en sorte que rejouer un rappel ne change rien. MĂȘme avec une seule tentative de livraison, votre propre infrastructure finira par confier le mĂȘme message Ă deux workers, et une commande qui crĂ©dite un client deux fois est un bien pire bug qu'une commande qui crĂ©dite en retard.
Enregistrement et secrets
L'enregistrement de l'URL de rappel, l'envoi d'une livraison de test signée et la rotation du secret de signature se font tous depuis votre panneau revendeur connecté, pas via l'API publique. HTTPS est obligatoire et les adresses privées sont refusées.
Ouvrir API Access dans le panneau revendeurLimites de débit et gestion des erreurs
Les limites sont publiĂ©es, appliquĂ©es par compte, et calculĂ©es sur une fenĂȘtre glissante. Le travail de lecture du catalogue bĂ©nĂ©ficie du plafond le plus Ă©levĂ© ; les endpoints de statut sont volontairement serrĂ©s, car ils existent comme filet de rapprochement et non comme boucle d'interrogation.
| Route | RequĂȘtes | FenĂȘtre | ComptĂ©es par |
|---|---|---|---|
| GET /catalog/* (all five) | 80 | 60s | per account |
| POST /orders/create | 120 | 60s | per account |
| GET /orders/:orderId | 3 | 60s | per account + order id |
| POST /orders/batch | 2 | 60s | per account |
| GET /orders | 2 | 60s | per account |
| GET /account | 60 | 60s | per account |
| POST /player/validate | dedicated limiter | â | per account |
Chaque rĂ©ponse porte X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset : un client bien Ă©levĂ© n'a donc jamais Ă deviner oĂč il en est. Surveillez le compteur restant et ralentissez avant d'ĂȘtre limitĂ©, pas aprĂšs.
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1765000000
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 45 seconds.",
"details": {
"limit": 80,
"remaining": 0,
"retry_after": 45,
"window_seconds": 60
}
}
}Les erreurs que vous rencontrerez en premier, et leurs correctifs
Les échecs renvoient toujours un code stable à cÎté du statut HTTP. Voici ceux qu'une nouvelle intégration rencontre dÚs sa premiÚre semaine, chacun accompagné du changement qui le résout.
| Code | HTTP | Signification | Comment le corriger |
|---|---|---|---|
| INVALID_API_KEY | 401 | Invalid API keyThe provided API key does not match any active key in the system. | Verify your key ID and secret are correct. Regenerate the key from the API Access panel if needed. |
| IP_NOT_ALLOWED | 403 | IP address not in allowlistThe request originated from an IP address not in your API key allowlist. | Add your server IP to the allowlist in the API Access panel, or remove IP restrictions. |
| MISSING_REQUIRED_FIELD | 400 | Missing required fieldA required field is missing from the request body. | Check the endpoint documentation for required fields. |
| INVALID_UUID_FORMAT | 400 | Invalid UUID formatThe order_id is not a valid UUID format. | Use any valid UUID format (v1, v4, v7, etc). Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. |
| PLAYER_NOT_FOUND | 400 | Player not foundThe player ID could not be found in the game system. | Verify the player ID is correct. Check if a zone_id is required for the game. |
| REGION_MISMATCH | 400 | Region mismatchThe player belongs to a different region than expected. | Use the correct zone_id or genshin_zone for the player region. |
| INSUFFICIENT_BALANCE | 400 | Insufficient wallet balanceYour wallet does not have enough funds to complete this order. | Top up your wallet from the Reload page before placing the order. |
| OUT_OF_STOCK | 400 | Product is out of stockThe requested product is currently unavailable. | Try again later or choose a different product. |
| DUPLICATE_ORDER | 409 | Duplicate order IDAn order with this UUID already exists. This is an idempotency protection. | Generate a new UUID for a new order. If retrying, use the same UUID to get the existing order. |
| PRICE_INCREASED | 400 | Price has increased beyond expectedThe current unit price is higher than the expected_unit_price you provided. | Fetch the latest price with GET /catalog/subcategory/:id/price and update expected_unit_price. |
| RATE_LIMIT_EXCEEDED | 429 | Rate limit exceededYou have exceeded the rate limit for this endpoint. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. When exceeded, the 429 response body contains: limit, remaining (0), retry_after (seconds to wait), and window_seconds. A Retry-After header is also set. | Wait for the number of seconds indicated by retry_after or the Retry-After header before retrying. Monitor X-RateLimit-Remaining headers to avoid hitting limits. |
| SERVICE_UNAVAILABLE | 503 | Service temporarily unavailableThe service is temporarily down for maintenance or overloaded. | Wait and retry after a few minutes. |
Questions d'intégration, avec des réponses
Les questions que les dĂ©veloppeurs posent vraiment pendant le branchement â authentification, relances, idempotence, limitation de dĂ©bit, rappels et gestion des Ă©checs.
Comment l'API revendeur authentifie-t-elle les requĂȘtes ?
Avec un seul en-tĂȘte HTTP : Authorization: Bearer <keyId>.<secret>. La paire de clĂ©s est dĂ©livrĂ©e depuis la page API Access de votre panneau revendeur, le secret n'est affichĂ© qu'une fois Ă la crĂ©ation, et le serveur le vĂ©rifie par un contrĂŽle HMAC Ă chaque appel. Pas de redirection OAuth, pas de cookie de session, aucun token Ă rafraĂźchir : le mĂȘme en-tĂȘte vaut pour la premiĂšre requĂȘte comme pour toutes les suivantes.
Vers quelle URL de base et quelle version mon client doit-il pointer ?
Une seule URL de base, et c'est celle de production : il n'existe pas d'hĂŽte bac Ă sable sĂ©parĂ© sur lequel dĂ©velopper puis basculer ensuite. Pointez le client vers l'URL de base v1 indiquĂ©e ci-dessus, envoyez Authorization: Bearer <keyId>.<secret> Ă chaque requĂȘte et commencez par GET /account pour vĂ©rifier la clĂ© et la forme de l'en-tĂȘte avant tout le reste. POST /orders/create est le seul appel qui dĂ©place de l'argent : gardez-le pour la fin, exĂ©cutez-le sur votre plus petite dĂ©nomination et envoyez expected_unit_price tant que le client est encore en rodage.
Quel champ porte la clé d'idempotence sur POST /orders/create ?
Le champ order_id â un UUID que vous gĂ©nĂ©rez cĂŽtĂ© client et envoyez dans le corps de la crĂ©ation. Rejouez une crĂ©ation avec un order_id dĂ©jĂ existant et l'API rĂ©pond HTTP 409 avec DUPLICATE_ORDER au lieu d'ouvrir une seconde commande : une relance ne peut donc jamais devenir un second dĂ©bit, et vous relisez la commande d'origine avec GET /orders/:orderId. GĂ©nĂ©rez et persistez l'UUID avant la premiĂšre tentative, car un UUID créé aprĂšs un timeout est une nouvelle clĂ© et non une clĂ© de relance, et rien cĂŽtĂ© serveur ne fusionnera deux order_id qui visaient la mĂȘme chose.
Ma requĂȘte a expirĂ©. La commande a-t-elle Ă©tĂ© créée ?
VĂ©rifiez par une lecture du statut plutĂŽt que de deviner. Appelez GET /orders/:orderId avec le mĂȘme UUID que celui envoyĂ© : si la commande revient, l'expiration a eu lieu aprĂšs son acceptation et vous ne devez pas renvoyer la requĂȘte. Si vous obtenez ORDER_NOT_FOUND, rien n'a Ă©tĂ© dĂ©bitĂ© et vous pouvez rĂ©pĂ©ter sans risque la requĂȘte de crĂ©ation avec ce mĂȘme UUID.
Comment vérifier la signature d'un callback webhook ?
Recalculez le HMAC sur le corps brut de la requĂȘte et comparez-le Ă l'en-tĂȘte X-Shop2Topup-Signature. L'en-tĂȘte contient sha256=<hex> : calculez le HMAC-SHA256 des octets exacts reçus avec votre secret de webhook, prĂ©fixez sha256=, puis comparez en temps constant â jamais contre un objet JSON re-sĂ©rialisĂ©, car parser puis re-sĂ©rialiser modifie les octets et la comparaison n'aboutira jamais. Rejetez un Ă©cart avec un 401, rĂ©pondez 200 Ă une livraison valide en moins de dix secondes, et faites le travail de rĂ©conciliation ensuite sur votre propre file.
Que se passe-t-il quand j'atteins une limite de débit ?
Vous obtenez un HTTP 429 avec le code RATE_LIMIT_EXCEEDED et un en-tĂȘte Retry-After. Le corps de la rĂ©ponse rĂ©pĂšte la mĂȘme valeur dans retry_after et y ajoute limit, remaining et window_seconds. Attendez exactement ce nombre de secondes et rĂ©essayez une fois ; ne relancez pas immĂ©diatement et ne rĂ©partissez pas le mĂȘme appel sur plusieurs clĂ©s, car le compteur est rattachĂ© au compte, pas Ă la connexion.
Quels champs POST /player/validate renvoie-t-il ?
player_name, ainsi que zone_id et region quand le jeu les expose, dans l'objet data de la rĂ©ponse. La crĂ©ation de commande rĂ©sout le joueur et contrĂŽle la rĂ©gion elle-mĂȘme : une tentative hors rĂ©gion est refusĂ©e avec REGION_MISMATCH (HTTP 400) avant tout dĂ©bit du portefeuille, donc ce n'est pas cet appel qui protĂšge votre solde. Appelez-le parce que relire player_name est le seul moyen concret de montrer Ă l'acheteur le compte qu'il s'apprĂȘte Ă recharger et d'attraper un identifiant mal saisi avant qu'il ne valide â le nom vient toujours du jeu, jamais de ce que l'acheteur a tapĂ©.
Comment transformer la réponse requirements en charge utile de commande ?
Indexez-la par field_name : chaque entrĂ©e renvoyĂ©e par GET /catalog/category/:categoryId/requirements devient une propriĂ©tĂ© de l'objet requirements que vous envoyez Ă POST /orders/create. Chaque entrĂ©e porte aussi data_type, ce qui dit Ă votre formulaire quoi afficher et Ă votre serveur quoi convertir, et les entrĂ©es single_select et multi_select embarquent leurs valeurs autorisĂ©es â proposez celles-ci plutĂŽt qu'un champ libre. Mettez la liste en cache par catĂ©gorie au lieu de la relire Ă chaque commande, et relisez-la quand un appel de crĂ©ation rĂ©pond MISSING_REQUIRED_FIELD.
Comment mon client doit-il traiter une réponse PRICE_INCREASED ?
Relisez le prix, dĂ©cidez, puis renvoyez â ne rejouez jamais le mĂȘme corps. PRICE_INCREASED (HTTP 400) signifie que le prix unitaire en vigueur dĂ©passe l'expected_unit_price envoyĂ© : la crĂ©ation a Ă©tĂ© refusĂ©e et rien n'a Ă©tĂ© dĂ©bitĂ© ; les dĂ©tails de l'erreur portent Ă la fois expected_unit_price et current_unit_price, et une relance aveugle avec l'ancien chiffre sera refusĂ©e Ă chaque fois. Lisez GET /catalog/subcategory/:itemId/price, dĂ©cidez si le nouveau chiffre vous convient toujours, et renvoyez la crĂ©ation avec l'expected_unit_price mis Ă jour et le mĂȘme order_id â une tentative refusĂ©e ne l'a pas consommĂ©. Omettre expected_unit_price dĂ©sactive complĂštement le garde-fou, ce qu'une intĂ©gration automatisĂ©e ne veut presque jamais.
Comment une commande partiellement livrée apparaßt-elle dans la réponse ?
En statut partial, avec le dĂ©tail dans sub_transaction_summary. Cet objet compte les unitĂ©s par Ă©tat â completed, refunded, failed, pending, processing, retrying â et sub_transactions porte la ligne derriĂšre chaque compteur, de sorte que votre logique de rĂ©conciliation lit des nombres au lieu de les dĂ©duire d'un seul mot. Traitez pending et processing comme non terminaux et continuez d'attendre ; traitez completed, partial, failed et refunded comme terminaux. Les callbacks order.completed, order.failed et order.refunded transportent le mĂȘme rĂ©sumĂ© : une charge utile dont la signature est vĂ©rifiĂ©e suffit Ă solder sans seconde lecture.
Que reçoit une requĂȘte venant d'une adresse absente de la liste autorisĂ©e ?
Un HTTP 403 avec le code IP_NOT_ALLOWED, renvoyĂ© avant toute logique mĂ©tier. Le contrĂŽle s'applique dĂšs que la liste d'IP autorisĂ©es de la clĂ© n'est pas vide, ce qui rend une clĂ© fuitĂ©e inerte ailleurs que sur vos propres adresses de sortie. Branchez sur error.code plutĂŽt que sur le texte du message, et lisez un IP_NOT_ALLOWED soudain en production comme un changement d'adresse de sortie â nouvelle passerelle NAT, nouveau sous-rĂ©seau de workers â plutĂŽt que comme une clĂ© cassĂ©e. Ne laissez la liste vide que si votre adresse sortante n'est vraiment pas stable.
Combien de temps une commande met-elle Ă se terminer ?
Traitez l'aboutissement comme asynchrone, jamais comme instantanĂ©. POST /orders/create rĂ©pond dĂšs que la commande est acceptĂ©e et le portefeuille dĂ©bitĂ©, normalement avec le statut pending ; l'exĂ©cution tourne ensuite sur des workers sĂ©parĂ©s et l'Ă©tat final vous parvient par webhook ou lors de votre prochaine lecture de statut. Ne bloquez pas une requĂȘte HTTP cĂŽtĂ© client sur l'Ă©tat final â acceptez-la, rĂ©pondez Ă votre client, et rĂ©glez le reste hors ligne.
Créez un compte, générez une clé, passez en production
Il n'y a ni file de validation ni dĂ©lai d'attente entre l'inscription et le premier appel Ă l'API. CrĂ©ez le compte revendeur, gĂ©nĂ©rez une clĂ© sur la page API Access, approvisionnez le portefeuille, et votre premiĂšre commande peut partir le jour mĂȘme.
Vous n'ĂȘtes pas le dĂ©veloppeur du projet ? Le volet commercial est traitĂ© sur la page du programme revendeur SHOP2TOPUP.