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",
"action": "FIX_INPUT",
"retryable": false,
"retry_after": null,
"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 | Récupérer les infos du compte | Lire la référence |
| GET | /catalog/big-categories | Lister les grandes catégories | Lire la référence |
| GET | /catalog/categories | Lister les catégories | Lire la référence |
| GET | /catalog/subcategories | Lister les sous-catégories (produits) | Lire la référence |
| GET | /catalog/subcategory/:itemId/price | Prix d'un article | Lire la référence |
| GET | /catalog/category/:categoryId/requirements | Obtenir les champs requis d'une catégorie | Lire la référence |
| POST | /player/validate | Valider un joueur | Lire la référence |
| POST | /orders/create | Créer une commande | Lire la référence |
| GET | /orders/:orderId | Statut de la commande | Lire la référence |
| POST | /orders/batch | Commandes en lot | Lire la référence |
| GET | /orders | Lister les commandes | 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 | Une unité de la commande a été livrée. Le statut global de la commande se lit dans data.status. |
| order.refunded | Une unité de la commande a été remboursée sur votre portefeuille, et data.status passe à refunded dÚs que toutes le sont. |
| webhook.test | Vous avez lancé depuis le panneau une livraison de test signée pour vérifier votre gestionnaire d'événements. |
En-tĂȘtes de livraison
| En-tĂȘte | Valeur |
|---|---|
| Content-Type | application/json |
| X-Shop2Topup-Signature | sha256=<hex> : HMAC-SHA256 du corps brut de la requĂȘte |
| X-Shop2Topup-Event | Nom de l'événement, order.completed par exemple |
| 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/* (les cinq) | 5000 | 60 s | par compte |
| POST /orders/create | 5000 | 60 s | par compte |
| GET /orders/:orderId | 3 | 60 s | par compte + ID de commande |
| POST /orders/batch | 2 | 60 s | par compte |
| GET /orders | 2 | 60 s | par compte |
| GET /account | 60 | 60 s | par compte |
| POST /player/validate | quota quotidien de vĂ©rifications joueur | 24 h | par compte (sans en-tĂȘtes X-RateLimit) |
Les endpoints catalogue, commandes et compte portent X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset sur chaque rĂ©ponse : 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. La validation de joueur fait exception : elle repose sur votre quota quotidien de vĂ©rifications, n'envoie aucun en-tĂȘte X-RateLimit et rapporte ce quota dans le corps du 429.
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1765000000
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again after retry_after seconds.",
"action": "RETRY_LATER",
"retryable": true,
"retry_after": 45,
"details": {
"limit": 2,
"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 | Clé API invalideLa clé API fournie ne correspond à aucune clé active. | Vérifiez votre ID de clé et votre secret. Au besoin, régénérez la clé depuis le panneau AccÚs API.action: FIX_INPUT · retryable: false · retry_after: null |
| IP_NOT_ALLOWED | 403 | Adresse IP absente de la liste autorisĂ©eLa requĂȘte provient d'une adresse IP qui ne figure pas dans la liste autorisĂ©e de votre clĂ© API. | Ajoutez l'IP de votre serveur Ă la liste autorisĂ©e dans le panneau AccĂšs API, ou supprimez les restrictions d'IP.action: FIX_INPUT · retryable: false · retry_after: null |
| MISSING_REQUIRED_FIELD | 400 | Champ obligatoire manquantUn champ obligatoire manque dans le corps de la requĂȘte. Pour les champs liĂ©s au joueur, details.field indique le champ manquant. | Consultez la documentation de l'endpoint, ainsi que GET /catalog/category/:categoryId/requirements pour les champs joueur, puis envoyez tous les champs obligatoires.action: FIX_INPUT · retryable: false · retry_after: null |
| INVALID_UUID_FORMAT | 400 | Format UUID invalideL'order_id n'est pas un UUID valide. | Utilisez n'importe quel format UUID valide (v1, v4, v7, etc.). Format : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.action: FIX_INPUT · retryable: false · retry_after: null |
| PLAYER_NOT_FOUND | 400 | Joueur introuvableLe jeu a confirmĂ© que cet ID joueur n'existe pas. Ce code n'apparaĂźt que sur une rĂ©ponse confirmĂ©e. Si l'ID ne peut pas ĂȘtre vĂ©rifiĂ©, vous recevez PLAYER_CHECK_UNAVAILABLE. | VĂ©rifiez l'ID joueur. Regardez aussi si le jeu exige un zone_id.action: FIX_INPUT · retryable: false · retry_after: null |
| PLAYER_CHECK_UNAVAILABLE | 503 | Impossible de vĂ©rifier les ID joueur de ce jeu pour le momentLes ID joueur de ce jeu ne peuvent pas ĂȘtre vĂ©rifiĂ©s en ce moment. L'ID joueur n'a PAS Ă©tĂ© refusĂ©. | Renvoyez la mĂȘme requĂȘte aprĂšs retry_after secondes. N'annoncez pas Ă votre client que son ID est faux.action: RETRY_LATER · retryable: true · retry_after: 60 |
| REGION_MISMATCH | 400 | La rĂ©gion de ce joueur ne peut pas recevoir ce produitLe joueur appartient Ă une rĂ©gion oĂč ce produit ne peut pas ĂȘtre livrĂ©. L'erreur contient player_region et player_name. | Choisissez le produit qui correspond Ă la rĂ©gion du joueur.action: CHOOSE_OTHER · retryable: false · retry_after: null |
| INSUFFICIENT_BALANCE | 400 | Solde du portefeuille insuffisantVotre portefeuille ne contient pas assez de fonds pour finaliser cette commande. | Alimentez votre portefeuille depuis la page de recharge avant de commander.action: TOP_UP · retryable: false · retry_after: null |
| OUT_OF_STOCK | 400 | Produit en rupture de stockCe produit est en rupture de stock pour le moment. | Réessayez plus tard ou choisissez un autre produit.action: RETRY_LATER · retryable: true · retry_after: null |
| PRODUCT_UNAVAILABLE | 409 | Ce produit est temporairement indisponibleLe produit existe, mais il ne peut pas ĂȘtre vendu pour le moment. | RĂ©essayez plus tard ou choisissez un autre produit.action: RETRY_LATER · retryable: true · retry_after: null |
| DUPLICATE_ORDER | 409 | ID de commande en doubleUne commande avec cet UUID existe déjà . Il s'agit d'une protection d'idempotence. | Générez un nouvel UUID pour une nouvelle commande. Si vous recevez cette erreur lors d'une nouvelle tentative, la premiÚre a abouti : consultez cette commande avec GET /orders/{orderId} au lieu d'en créer une autre.action: CHECK_ORDER · retryable: false · retry_after: null |
| PRICE_INCREASED | 400 | Le prix a augmenté au-delà de la valeur attendueLe prix unitaire actuel est supérieur à la valeur expected_unit_price que vous avez fournie. | Récupérez le dernier prix avec GET /catalog/subcategory/:id/price, puis mettez à jour expected_unit_price.action: FIX_INPUT · retryable: false · retry_after: null |
| RATE_LIMIT_EXCEEDED | 429 | Limite de requĂȘtes dĂ©passĂ©eVous avez dĂ©passĂ© la limite de cet endpoint. Les quotas par minute (catalog, orders, account) renvoient X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset Ă chaque rĂ©ponse, et details reprend limit, remaining (0), retry_after et window_seconds. POST /player/validate est dĂ©comptĂ© sur votre quota quotidien de vĂ©rifications joueur : il n'envoie aucun en-tĂȘte X-RateLimit, et sa rĂ©ponse 429 contient limit, current, remaining, reset_at et reset_in_seconds dans error. Dans les deux cas, error.retry_after et l'en-tĂȘte Retry-After indiquent le nombre de secondes Ă attendre. | Attendez le nombre de secondes indiquĂ© dans retry_after ou dans l'en-tĂȘte Retry-After avant de rĂ©essayer. Sur les endpoints limitĂ©s Ă la minute, surveillez X-RateLimit-Remaining pour Ă©viter d'atteindre la limite.action: RETRY_LATER · retryable: true · retry_after: secondes avant la rĂ©initialisation |
| INTERNAL_ERROR | 500 | Erreur interne du serveurUne erreur inattendue s'est produite sur le serveur. | Renvoyez la requĂȘte aprĂšs retry_after secondes. Pour POST /orders/create, rĂ©utilisez le mĂȘme order_id : la commande ne pourra ainsi jamais ĂȘtre créée deux fois. Si le problĂšme persiste, contactez le support.action: RETRY_LATER · retryable: true · retry_after: 30 |
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 comme completed, refunded ou pending â ces trois compteurs totalisent toujours total â et sub_transactions liste chaque unitĂ© avec son propre statut. Une unitĂ© qui n'a encore Ă©tĂ© ni livrĂ©e ni remboursĂ©e reste pending. Traitez pending comme non terminal et continuez d'attendre ; traitez completed, partial et refunded comme terminaux. Les compteurs failed, processing et retrying ne sont conservĂ©s que pour la compatibilitĂ© et valent toujours 0. Les callbacks order.completed 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.