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.

RequĂȘte - cURL
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
  -H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"
Réponse - 200 OK
{
  "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.

Réponse d'erreur
{
  "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éthodeCheminRÎleRéférence
GET/accountRécupérer les infos du compteLire la référence
GET/catalog/big-categoriesLister les grandes catégoriesLire la référence
GET/catalog/categoriesLister les catégoriesLire la référence
GET/catalog/subcategoriesLister les sous-catégories (produits)Lire la référence
GET/catalog/subcategory/:itemId/pricePrix d'un articleLire la référence
GET/catalog/category/:categoryId/requirementsObtenir les champs requis d'une catégorieLire la référence
POST/player/validateValider un joueurLire la référence
POST/orders/createCréer une commandeLire la référence
GET/orders/:orderIdStatut de la commandeLire la référence
POST/orders/batchCommandes en lotLire la référence
GET/ordersLister les commandesLire 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.

En-tĂȘte de requĂȘte
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 compte

Une 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. 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. 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. 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. 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 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 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 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Ă©nementDĂ©clenchĂ© quand
order.completedUne unité de la commande a été livrée. Le statut global de la commande se lit dans data.status.
order.refundedUne unité de la commande a été remboursée sur votre portefeuille, et data.status passe à refunded dÚs que toutes le sont.
webhook.testVous 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ĂȘteValeur
Content-Typeapplication/json
X-Shop2Topup-Signaturesha256=<hex> : HMAC-SHA256 du corps brut de la requĂȘte
X-Shop2Topup-EventNom de l'événement, order.completed par exemple
User-Agentshop2topup-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.

Corps du POST - order.completed
{
  "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Ă©.

Node.js - vérifier et acquitter
// 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 revendeur

Limites 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.

RouteRequĂȘtesFenĂȘtreComptĂ©es par
GET /catalog/* (les cinq)500060 spar compte
POST /orders/create500060 spar compte
GET /orders/:orderId360 spar compte + ID de commande
POST /orders/batch260 spar compte
GET /orders260 spar compte
GET /account6060 spar compte
POST /player/validatequota quotidien de vĂ©rifications joueur24 hpar 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.

Réponse limitée - 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.

CodeHTTPSignificationComment le corriger
INVALID_API_KEY401Clé 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_ALLOWED403Adresse 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_FIELD400Champ 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_FORMAT400Format 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_FOUND400Joueur 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_UNAVAILABLE503Impossible 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_MISMATCH400La 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_BALANCE400Solde 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_STOCK400Produit 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_UNAVAILABLE409Ce 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_ORDER409ID 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_INCREASED400Le 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_EXCEEDED429Limite 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_ERROR500Erreur 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
Voir la rĂ©fĂ©rence complĂšte des codes d'erreur — les 42 codes et leurs correctifs

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.

API de recharge de jeux pour revendeurs — Documentation