Aller au contenu

Technique

Intégrer l’API d’un panel SMM : un tutoriel qui fonctionne

Mis à jour le par La rédaction de PanelCompare, 7 min de lecture

De quoi avez-vous besoin avant d’écrire la moindre ligne de code ?

D’un compte sur le panel, d’un solde alimenté et d’une clé API récupérée dans le tableau de bord du panel. Il n’existe ni bac à sable, ni clé de test, ni environnement de préproduction nulle part sur ce marché : la première commande réussie en développement est une vraie commande, qui coûte vraiment de l’argent et livre vraiment sur le lien que vous y avez mis. Utilisez un lien qui vous appartient et auquel vous ne tenez pas.

Tenez compte aussi de la taille du catalogue. Les vrais catalogues comptent d’environ 3 000 à 8 000 lignes, et l’action services les renvoie toutes en une seule réponse sans pagination : c’est un appel coûteux, à planifier à intervalles réguliers plutôt qu’à placer sur le chemin d’une requête. Les deux panels synchronisés dans l’indice PanelCompare renvoient respectivement 5 558 et 2 196 lignes rattachées (indice des prix PanelCompare, 10 septembre 2026).

Comment vérifier que le point d’accès existe avant de s’authentifier ?

Envoyez l’action services sans aucune clé. Un vrai point d’accès API v2 répond par une erreur structurée, ce qui prouve qu’il existe. Une erreur 404 ou une réponse en HTML dément l’API que presque tous les panels annoncent en page d’accueil. C’est ainsi qu’on peut vérifier un panel sans jamais détenir d’identifiants chez lui.

Sonder un point d’accès avec curl, sans clé
# A real endpoint answers with JSON, even without a key.
curl -s -X POST https://example-panel.com/api/v2 \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "action=services"

# Endpoint present:
# {"error":"Invalid API key"}

# Endpoint absent (HTTP 404, or an HTML login page):
# <!DOCTYPE html> ...

# Panels vary on the path. Try these three, in order:
#   /api/v2      the convention
#   /api/v2.php  older builds
#   /api         a minority

Il arrive qu’un appel sans clé renvoie un tableau plutôt qu’une erreur. Ce n’est pas un bug, c’est un catalogue ouvert : le panel publie toute sa liste de services à qui la demande. C’est assez rare pour mériter d’être noté quand vous le rencontrez.

Quels sont les quatre comportements qui font échouer une première intégration ?

Les échecs qui tiennent à la spécification, pas au panel
ComportementCe que fait un code naïfCe que fait un code correct
Les erreurs renvoient HTTP 200Vérifie res.ok, obtient true et traite un objet d’erreur comme un résultatAnalyse d’abord le corps et cherche une clé error avant toute autre chose
Certains panels renvoient une erreur 4xx avec une erreur JSONPasse au chemin suivant sur la seule foi du statut, et signale une mauvaise clé comme une API manquanteAnalyse aussi le corps des réponses 4xx ; un corps qui contient une clé error signale un point d’accès qui fonctionne
Les nombres arrivent sous forme de chaînesCompare numériquement rate, min, max ou remains et donne des résultats faux, sans erreur visibleConvertit explicitement chaque champ numérique dès l’analyse de la réponse
Le statut multiple est un paramètre au pluriel, pas une actionAppelle status une fois par commande et se fait limiter en débitEnvoie jusqu’à 100 ids séparés par des virgules dans le paramètre orders de la même action status

Source : Comportements vérifiés sur justanotherpanel.com/api et mis en œuvre dans le client de synchronisation de PanelCompare (src/lib/sync/panel-api.ts), 11 septembre 2026.

Le cas 401 est celui qui vous coûte un ticket de support

JustAnotherPanel répond à une mauvaise clé par HTTP 401 et un corps JSON de la forme {"error":"Invalid API key"} : une réponse d’API structurée qui se trouve porter un statut d’erreur. Un client qui traite toute réponse 4xx comme « pas d’API ici » dira au propriétaire d’un panel que son panel n’a pas de point d’accès, alors que le vrai problème est la clé. Analysez le corps avant de tirer une conclusion du statut.

À quoi ressemble un client correct en Node ?

Un client API v2 minimal avec la gestion d’erreurs requise (Node 18+, sans dépendance)
const PATHS = ["/api/v2", "/api/v2.php", "/api"];

async function call(domain, key, params, path = PATHS[0]) {
  const res = await fetch("https://" + domain + path, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({ key, ...params }).toString(),
  });

  const text = await res.text();
  let body;
  try {
    body = JSON.parse(text);
  } catch {
    // HTML here usually means the login page: the path is wrong, not the key.
    throw new Error("Not JSON at " + path + " (HTTP " + res.status + ")");
  }

  // Errors arrive as HTTP 200 with an error field, and sometimes as 4xx with
  // the same body. Either way the body is what decides.
  if (body && typeof body === "object" && "error" in body) {
    throw new Error("API error: " + body.error);
  }
  if (res.status >= 400) throw new Error("HTTP " + res.status);

  return body;
}

const num = (v) => (v === null || v === undefined ? null : Number(v));

async function services(domain, key) {
  const rows = await call(domain, key, { action: "services" });
  // Every numeric field is a string on the wire. Coerce at the boundary.
  return rows.map((r) => ({
    id: String(r.service),
    name: r.name,
    type: r.type,
    rate: num(r.rate),
    min: num(r.min),
    max: num(r.max),
    refill: Boolean(r.refill),
    cancel: Boolean(r.cancel),
  }));
}

async function addOrder(domain, key, { service, link, quantity, runs, interval }) {
  const params = { action: "add", service: String(service), link };
  if (quantity != null) params.quantity = String(quantity);
  // Drip-feed: quantity is PER RUN. Total delivered and charged is quantity x runs.
  if (runs != null) params.runs = String(runs);
  if (interval != null) params.interval = String(interval);
  const body = await call(domain, key, params);
  return String(body.order);
}

async function statusMany(domain, key, orderIds) {
  const out = {};
  // The plural parameter is capped at 100 ids. There is no multi_status action.
  for (let i = 0; i < orderIds.length; i += 100) {
    const chunk = orderIds.slice(i, i + 100);
    const body = await call(domain, key, { action: "status", orders: chunk.join(",") });
    for (const [id, row] of Object.entries(body)) {
      out[id] = row && row.error
        ? { error: row.error }
        : {
            status: row.status,
            charge: num(row.charge),
            startCount: num(row.start_count),
            remains: num(row.remains),
            currency: row.currency,
          };
    }
  }
  return out;
}

Trois détails de ce code en font tout l’intérêt. Le corps est analysé avant que le statut ne soit consulté. La branche 4xx s’exécute après la vérification de la clé error, et non avant. Et chaque champ numérique passe par une conversion explicite, car rate, min, max, charge, start_count et remains arrivent tous sous forme de chaînes entre guillemets.

À quoi ressemble le même client en Python et en PHP ?

Python, avec requests
import requests

PATHS = ["/api/v2", "/api/v2.php", "/api"]


class PanelError(Exception):
    pass


def call(domain, key, params, path=PATHS[0], timeout=30):
    res = requests.post(
        "https://" + domain + path,
        data={"key": key, **params},
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        timeout=timeout,
    )

    try:
        body = res.json()
    except ValueError:
        raise PanelError("Not JSON at %s (HTTP %s)" % (path, res.status_code))

    # 200 with an error field is the normal failure shape. Some panels use 401
    # with the same body, so the body is checked before the status.
    if isinstance(body, dict) and "error" in body:
        raise PanelError(body["error"])
    if res.status_code >= 400:
        raise PanelError("HTTP %s" % res.status_code)

    return body


def services(domain, key):
    rows = call(domain, key, {"action": "services"})
    return [
        {
            "id": str(r["service"]),
            "name": r["name"],
            "rate": float(r["rate"]),
            "min": int(r["min"]),
            "max": int(r["max"]),
            "refill": bool(r.get("refill")),
        }
        for r in rows
    ]


def status_many(domain, key, order_ids):
    out = {}
    for i in range(0, len(order_ids), 100):  # the plural parameter caps at 100
        chunk = ",".join(str(o) for o in order_ids[i : i + 100])
        out.update(call(domain, key, {"action": "status", "orders": chunk}))
    return out
PHP, avec curl
<?php
function panel_call(string $domain, string $key, array $params, string $path = "/api/v2") {
    $ch = curl_init("https://" . $domain . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query(array_merge(["key" => $key], $params)),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
    ]);

    $text = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $body = json_decode($text, true);
    if ($body === null) {
        throw new RuntimeException("Not JSON at {$path} (HTTP {$code})");
    }
    // Body before status: errors come back as 200, and sometimes as 401.
    if (is_array($body) && isset($body["error"])) {
        throw new RuntimeException("API error: " . $body["error"]);
    }
    if ($code >= 400) {
        throw new RuntimeException("HTTP {$code}");
    }
    return $body;
}

// Placing an order. Numeric fields come back as strings; cast what you compare.
$order = panel_call($domain, $key, [
    "action"   => "add",
    "service"  => "1",
    "link"     => "https://example.com/p/abc",
    "quantity" => "1000",
]);
$orderId = (string) $order["order"];

Comment savoir de quels paramètres une commande a besoin ?

Par le champ type de la ligne du catalogue, le seul endroit où l’exigence est exprimée. Tout appel add exige service et link ; le reste ne se devine pas, et envoyer le mauvais jeu de paramètres produit une erreur plutôt qu’une valeur par défaut raisonnable. Lisez le type avant de construire le formulaire.

Aiguiller selon le type de la ligne
function paramsForType(row, input) {
  switch (row.type) {
    case "Default":
    case "Drip-feed":
      // runs and interval are optional; quantity is PER RUN when runs is set.
      return { quantity: input.quantity, runs: input.runs, interval: input.interval };

    case "Custom Comments":
    case "Custom Comments Package":
      return { comments: input.comments.join("\n") };

    case "Mentions User Followers":
      return { quantity: input.quantity, username: input.username };

    case "Mentions Hashtag":
      return { quantity: input.quantity, hashtag: input.hashtag };

    case "Mentions Media Likers":
      return { quantity: input.quantity, media: input.mediaUrl };

    case "Poll":
      return { quantity: input.quantity, answer_number: input.answerNumber };

    case "Subscriptions":
      return {
        username: input.username,
        min: input.min,
        max: input.max,
        posts: input.posts,
        delay: input.delay,
        expiry: input.expiry,
      };

    case "Package":
      return {}; // link only; the quantity is fixed by the package

    default:
      throw new Error("Unhandled order type: " + row.type);
  }
}

La branche default compte plus qu’il n’y paraît. Les panels ajoutent des types de commande, et un client qui se rabat sans rien dire sur une forme « quantité seule » passera des commandes mal formées qui échoueront après le débit du solde. Lever une erreur sur un type inconnu est la version bon marché de la correction de ce bug.

Comment interroger le statut des commandes ?

  1. 1.Regroupez les appels via le paramètre orders au pluriel, 100 ids à la fois. La spécification de référence ne comporte pas d’action multi_status, et c’est en envoyant une requête par commande que les intégrations se font limiter en débit.
  2. 2.Traitez Partial comme un résultat à part entière plutôt que comme une erreur. Il s’accompagne d’une quantité restante (remains) et d’un crédit automatique sur le solde, et c’est le résultat non définitif le plus courant sur ce marché.
  3. 3.Interrogez à un rythme proportionnel au délai de démarrage annoncé, pas à intervalle court et fixe. La plupart des lignes publient un délai ; presque aucune ne publie de vitesse.
  4. 4.Enregistrez l’identité du panel à côté de chaque id de commande. Les ids de commande sont uniques au sein d’un panel, et nulle part ailleurs.
  5. 5.Resynchronisez le catalogue à intervalles réguliers. Les ids de service sont propres à chaque panel et peuvent changer : un id enregistré peut discrètement se mettre à pointer vers un autre stock.
  6. 6.N’enregistrez jamais le corps des requêtes dans vos journaux. La clé s’y trouve, et aucune expiration ne vous sauvera.

Le vocabulaire des statuts est réduit et stable : Pending, In progress ou Processing, Completed, Partial, Canceled. Tout statut hors de cette liste doit être remonté tel quel plutôt que rattaché à la valeur connue la plus proche, car un panel qui renvoie un statut inattendu vous dit en général quelque chose que la correspondance masquerait.

Quelles contraintes de sécurité la spécification impose-t-elle ?

La clé circule dans le corps de la requête sans signature, sans nonce et sans horodatage, et la spécification ne définit aucune expiration. C’est donc un secret au porteur de longue durée : quiconque la détient peut dépenser le solde et lire tous les liens commandés depuis le compte. Aucune protection contre la réutilisation ne limite les dégâts, et la seule révocation possible consiste à changer la clé dans le tableau de bord.

  • N’acceptez jamais une clé venant d’un navigateur, et ne la faites jamais transiter par du code côté client.
  • N’enregistrez jamais le corps des requêtes dans vos journaux, et vérifiez que votre bibliothèque HTTP ne le fait pas à votre place.
  • Gardez les clés hors des variables d’environnement qui apparaissent dans un journal de build, et hors de la base de données.
  • Changez-les après chaque modification d’une intégration tierce, et après tout changement dans l’équipe.
  • Considérez une clé comme compromise dès qu’elle apparaît dans une capture d’écran, un ticket ou un document partagé.

Réponses rapides

Pourquoi l’API des panels SMM renvoie-t-elle 200 en cas d’erreur ?

Parce que la spécification n’utilise pas les codes de statut HTTP de façon significative. Les échecs reviennent généralement en HTTP 200 avec un objet d’erreur dans le corps, et certains panels répondent à une clé invalide par un 401 avec le même JSON : chaque client doit donc analyser le corps pour détecter un échec. Vérifier res.ok seul ne vous apprend rien.

Existe-t-il un bac à sable ou une clé de test pour les API des panels SMM ?

Non. Il n’existe aucun bac à sable sur ce marché : le premier appel add réussi est une vraie commande sur un vrai solde. Développez avec la plus petite quantité que permet la ligne, sur un lien qui vous appartient.

Comment vérifier le statut de nombreuses commandes à la fois ?

Utilisez la même action status avec un paramètre orders au pluriel contenant jusqu’à 100 ids séparés par des virgules. La spécification de référence ne comporte pas d’action multi_status distincte.

Pourquoi ma commande en drip-feed me coûte-t-elle dix fois ce que je pensais ?

Parce qu’en drip-feed, la quantité vaut par passage. Le total livré et facturé est égal à la quantité multipliée par le nombre de passages : 1 000 avec runs=10 revient à commander et à payer 10 000 unités.

Peut-on réutiliser un même client sur différents panels ?

Oui, en changeant l’URL de base et la clé : c’est la conséquence pratique du fait que tout le marché implémente une seule spécification. Ce qui ne suit pas, c’est la correspondance des ids de service, car les ids sont propres à chaque panel et peuvent être redirigés.

Chaque chiffre ici est sourcé et daté

Sur ce marché, les prix bougent chaque semaine : un chiffre sans date de relevé n’est qu’un décor. Quand ce guide cite un chiffre, il en donne la source et la date de vérification. Si l’un d’eux est faux, la procédure de correction décrite sur la page À propos prévoit une réponse sous deux jours ouvrés, et les corrections sont publiées avec une note datée plutôt que glissées en silence.