Zum Inhalt springen

Technik

SMM-Panel-API anbinden: eine funktionierende Anleitung

Zuletzt aktualisiert am von der Redaktion PanelCompare · 6 Min. Lesezeit

Was brauchen Sie, bevor Sie Code schreiben?

Ein Konto beim Panel, ein aufgeladenes Guthaben und einen API-Schlüssel aus dem Dashboard des Panels. In diesem Markt gibt es nirgends eine Sandbox, einen Testschlüssel oder eine Staging-Umgebung. Die erste erfolgreiche Bestellung in der Entwicklung ist also eine echte Bestellung, die echtes Geld kostet und tatsächlich an den eingegebenen Link liefert. Verwenden Sie einen Link, der Ihnen gehört und der Ihnen nicht wichtig ist.

Planen Sie auch die Größe des Katalogs ein. Echte Kataloge umfassen rund 3.000 bis 8.000 Zeilen, und die Aktion services liefert sie alle in einer einzigen Antwort ohne Seitenaufteilung. Sie ist also ein teurer Aufruf, der in einen Zeitplan gehört und nicht in den Ablauf einer Anfrage. Die beiden Panels, die in den Index von PanelCompare synchronisiert werden, liefern 5.558 beziehungsweise 2.196 zugeordnete Zeilen (Preisindex von PanelCompare, 10. September 2026).

Wie bestätigen Sie vor der Authentifizierung, dass der Endpunkt existiert?

Senden Sie die Aktion services ganz ohne Schlüssel. Ein echter Endpunkt für API v2 antwortet mit einer strukturierten Fehlermeldung, und das beweist, dass er existiert. Ein 404 oder eine HTML-Antwort widerlegt die API-Behauptung, die fast jedes Panel auf seiner Startseite aufstellt. So lässt sich ein Panel prüfen, ohne je Zugangsdaten dafür zu besitzen.

Einen Endpunkt mit curl prüfen, ohne Schlüssel
# 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

Gelegentlich liefert ein Aufruf ohne Schlüssel ein Array statt einer Fehlermeldung. Das ist kein Fehler, sondern ein offener Katalog: Das Panel veröffentlicht seine ganze Serviceliste für jeden. Das ist selten genug, um es zu notieren, wenn man darauf stößt.

Welche vier Verhaltensweisen lassen eine erste Integration scheitern?

Fehler, die in der Spezifikation liegen, nicht beim einzelnen Panel
VerhaltenWas naiver Code tutWas korrekter Code tut
Fehler kommen mit HTTP 200Prüft res.ok, sieht true und behandelt ein Fehlerobjekt als ErgebnisWertet zuerst den Antworttext aus und prüft vor allem anderen auf einen Schlüssel error
Manche Panels liefern 4xx mit einem JSON-FehlerSpringt allein wegen des Status zum nächsten Pfad und meldet einen falschen Schlüssel als fehlende APIWertet auch den 4xx-Antworttext aus; ein Antworttext mit einem Schlüssel error ist ein funktionierender Endpunkt
Zahlen kommen als ZeichenkettenVergleicht rate, min, max oder remains numerisch und liefert stillschweigend falsche ErgebnisseWandelt jedes numerische Feld beim Einlesen ausdrücklich um
Der Status mehrerer Bestellungen ist ein Parameter im Plural, keine AktionRuft status einmal pro Bestellung auf und läuft in ein Rate-LimitSendet bis zu 100 kommagetrennte IDs im Parameter orders derselben Aktion status

Quelle: Verhalten geprüft anhand von justanotherpanel.com/api und umgesetzt im Synchronisierungs-Client von PanelCompare (src/lib/sync/panel-api.ts), 11. September 2026.

Der Fall 401 ist der, der Sie ein Support-Ticket kostet

JustAnotherPanel beantwortet einen falschen Schlüssel mit HTTP 401 und einem JSON-Antworttext der Form {"error":"Invalid API key"} – eine strukturierte API-Antwort, die zufällig einen Fehlerstatus trägt. Ein Client, der jeden 4xx als „hier gibt es keine API“ behandelt, teilt einem Panel-Inhaber mit, sein Panel habe keinen Endpunkt, obwohl in Wahrheit der Schlüssel das Problem ist. Werten Sie den Antworttext aus, bevor Sie aus dem Status Schlüsse ziehen.

Wie sieht ein korrekter Client in Node aus?

Ein minimaler Client für API v2 mit der nötigen Fehlerbehandlung (Node 18+, ohne Abhängigkeiten)
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;
}

Drei Details in diesem Code sind der ganze Punkt. Der Antworttext wird ausgewertet, bevor der Status zählt. Der Zweig für 4xx läuft nach der Prüfung auf den Schlüssel error, nicht davor. Und jedes numerische Feld wird ausdrücklich umgewandelt, weil rate, min, max, charge, start_count und remains alle als Zeichenketten in Anführungszeichen ankommen.

Wie sieht derselbe Client in Python und PHP aus?

Python, mit 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, mit 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"];

Woher wissen Sie, welche Parameter eine Bestellung braucht?

Aus dem Feld type der Katalogzeile – der einzigen Stelle, an der die Anforderung steht. Jeder Aufruf von add braucht service und link; was sonst nötig ist, lässt sich nicht erraten, und wer die falschen Parameter sendet, bekommt eine Fehlermeldung statt eines sinnvollen Standardwerts. Lesen Sie den Typ, bevor Sie das Formular bauen.

Nach dem Typ der Zeile verzweigen
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);
  }
}

Der Zweig default ist wichtiger, als er aussieht. Panels fügen neue Bestelltypen hinzu, und ein Client, der stillschweigend auf eine Form nur mit Menge zurückfällt, gibt fehlerhafte Bestellungen auf, die scheitern, nachdem das Guthaben bereits belastet wurde. Bei einem unbekannten Typ einen Fehler zu werfen, ist die günstige Variante dieses Fehlers.

Wie sollte der Bestellstatus abgefragt werden?

  1. 1.Über den Parameter orders im Plural bündeln, 100 IDs auf einmal. Die maßgebliche Spezifikation kennt keine Aktion multi_status, und eine Anfrage pro Bestellung ist genau der Weg, auf dem Integrationen in ein Rate-Limit laufen.
  2. 2.Partial als vollwertiges Ergebnis behandeln, nicht als Fehler. Es kommt mit einer Restmenge (remains) und einer automatischen Gutschrift aufs Guthaben und ist in diesem Markt das häufigste Ergebnis, das nicht Completed lautet.
  3. 3.Die Abfragehäufigkeit an der angegebenen Startzeit ausrichten, nicht an einem festen kurzen Intervall. Die meisten Zeilen nennen eine Startzeit, fast keine eine Liefergeschwindigkeit.
  4. 4.Zu jeder Bestell-ID die Identität des Panels speichern. Bestell-IDs sind innerhalb eines Panels eindeutig und sonst nirgends.
  5. 5.Den Katalog regelmäßig neu synchronisieren. Service-IDs gelten nur im jeweiligen Panel und können sich ändern, sodass eine gespeicherte ID unbemerkt auf anderen Bestand zeigen kann.
  6. 6.Den Anfragetext nie protokollieren. Der Schlüssel steht darin, und kein Ablaufdatum rettet Sie.

Das Statusvokabular ist klein und stabil: Pending, In progress oder Processing, Completed, Partial, Canceled. Alles außerhalb dieser Menge sollte sichtbar gemacht und nicht auf den nächstliegenden bekannten Wert abgebildet werden, denn ein Panel mit einem unerwarteten Status sagt Ihnen meist etwas, das die Abbildung verdecken würde.

Welche Sicherheitsvorgaben ergeben sich aus der Spezifikation?

Der Schlüssel wird im Anfragetext übertragen, ohne Signatur, ohne Nonce und ohne Zeitstempel, und die Spezifikation sieht kein Ablaufdatum vor. Damit ist er ein langlebiges Bearer-Geheimnis: Wer ihn hat, kann das Guthaben ausgeben und jeden Link lesen, für den über das Konto bestellt wurde. Kein Schutz vor Replay-Angriffen begrenzt den Schaden, und widerrufen lässt er sich nur, indem man den Schlüssel im Dashboard erneuert.

  • Nie einen Schlüssel aus einem Browser annehmen und nie einen über clientseitigen Code weiterreichen.
  • Den Anfragetext nie protokollieren und prüfen, dass Ihre HTTP-Client-Bibliothek ihn nicht von sich aus protokolliert.
  • Schlüssel aus Umgebungsvariablen heraushalten, die in einem Build-Log landen, und aus der Datenbank.
  • Nach jeder Änderung an einer Integration eines Drittanbieters und nach jedem Personalwechsel erneuern.
  • Von einer Kompromittierung ausgehen, sobald ein Schlüssel in einem Screenshot, einem Ticket oder einem geteilten Dokument auftaucht.

Schnelle Antworten

Warum liefert die API eines SMM-Panels bei Fehlern 200 zurück?

Weil die Spezifikation HTTP-Statuscodes nicht sinnvoll nutzt. Fehler kommen meist als HTTP 200 mit einem Fehlerobjekt im Antworttext zurück, und manche Panels beantworten einen ungültigen Schlüssel mit 401 und demselben JSON. Deshalb muss jeder Client den Antworttext auswerten, um Fehler zu erkennen. Die Prüfung von res.ok allein sagt nichts aus.

Gibt es für SMM-Panel-APIs eine Sandbox oder einen Testschlüssel?

Nein. In diesem Markt gibt es nirgends eine Sandbox, der erste erfolgreiche Aufruf von add ist also eine echte Bestellung zulasten eines echten Guthabens. Entwickeln Sie mit der kleinsten Menge, die die Zeile zulässt, auf einem Link, der Ihnen gehört.

Wie frage ich den Status vieler Bestellungen auf einmal ab?

Mit derselben Aktion status und dem Parameter orders im Plural, der bis zu 100 kommagetrennte IDs aufnimmt. Eine eigene Aktion multi_status gibt es in der maßgeblichen Spezifikation nicht.

Warum kostet meine Drip-Feed-Bestellung das Zehnfache des Erwarteten?

Weil die Menge bei einer Drip-Feed-Bestellung pro Lauf gilt. Geliefert und berechnet wird Menge mal Läufe; 1.000 mit runs=10 bestellt und bezahlt also 10.000 Einheiten.

Kann ich einen Client für verschiedene Panels wiederverwenden?

Ja, mit geänderter Basis-URL und neuem Schlüssel – die praktische Folge davon, dass der ganze Markt eine einzige Spezifikation umsetzt. Nicht übertragbar ist die Zuordnung der Service-IDs, denn IDs gelten nur im jeweiligen Panel und können umgeleitet werden.

Jede Zahl hier hat eine Quelle und ein Datum

Die Preise in diesem Markt ändern sich wöchentlich, eine Zahl ohne Erhebungsdatum ist also nur Dekoration. Wo dieser Ratgeber eine Zahl zitiert, nennt er die Quelle und das Prüfdatum. Ist eine davon falsch, sieht das Korrekturverfahren auf der Seite Über uns eine Antwort innerhalb von zwei Werktagen vor, und Korrekturen werden mit einem datierten Hinweis veröffentlicht, statt stillschweigend eingespielt zu werden.