İçeriğe geç

Teknik

SMM panel API entegrasyonu: çalışan bir adım adım rehber

Son güncelleme: · Hazırlayan: PanelCompare editör ekibi · 5 dk okuma

Kod yazmadan önce neye ihtiyacınız var?

Panelde bir hesap, yüklenmiş bir bakiye ve panelin yönetim ekranından alınmış bir API anahtarı. Bu piyasanın hiçbir yerinde sandbox, test anahtarı ya da hazırlık ortamı yoktur; yani geliştirme sırasındaki ilk başarılı sipariş, gerçekten para tutan ve içine koyduğunuz linke gerçekten teslimat yapan gerçek bir sipariştir. Size ait olan ve umursamadığınız bir link kullanın.

Katalog büyüklüğünü de hesaba katın. Gerçek kataloglar kabaca 3.000 ile 8.000 servis arasındadır ve services eylemi hepsini sayfalanmamış tek bir yanıtta döndürür; yani bu, bir istek akışına değil, bir zamanlamaya ait pahalı bir çağrıdır. PanelCompare endeksine eşitlenen iki panel sırasıyla 5.558 ve 2.196 eşleştirilmiş kayıt döndürüyor (PanelCompare fiyat endeksi, 2026-09-10).

Kimlik doğrulamadan önce uç noktanın var olduğunu nasıl doğrularsınız?

services eylemini hiç anahtar olmadan gönderin. Gerçek bir API v2 uç noktası yapılandırılmış bir hatayla yanıt verir; bu da var olduğunu kanıtlar. 404 ya da bir HTML gövdesi ise neredeyse her panelin ana sayfasında yaptığı API iddiasını çürütür. Bir panel, onun için hiçbir giriş bilgisi tutmadan bu yolla doğrulanabilir.

curl ile uç nokta yoklaması, anahtar gerekmez
# 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

Anahtarsız bir çağrı ara sıra bir hata yerine bir dizi döndürür. Bu bir hata değil, açık bir katalogdur: panel bütün servis listesini herkese yayımlıyordur. Karşılaştığınızda kaydetmeye değecek kadar nadirdir.

Hangi dört davranış ilk entegrasyonu bozar?

Panel düzeyinde değil, spesifikasyon düzeyinde olan hatalar
DavranışBasit kodun yaptığıDoğru kodun yaptığı
Hatalar HTTP 200 dönerres.ok değerine bakar, true görür ve bir hata nesnesini sonuç sanarÖnce gövdeyi ayrıştırır ve her şeyden önce bir error anahtarı olup olmadığına bakar
Bazı paneller JSON hatasıyla 4xx dönerYalnızca duruma bakıp bir sonraki yola geçer ve yanlış anahtarı eksik bir API olarak raporlar4xx gövdesini de ayrıştırır; error anahtarı olan bir gövde, çalışan bir uç nokta demektir
Sayılar metin olarak gelirrate, min, max ya da remains alanlarını sayısal olarak karşılaştırır ve sessizce yanlış sonuç verirHer sayısal alanı ayrıştırma sınırında açıkça dönüştürür
Çoklu durum sorgusu bir eylem değil, çoğul bir parametredirHer sipariş için ayrı status çağırır ve istek sınırına takılırAynı status eyleminin orders parametresinde virgülle ayrılmış en fazla 100 id gönderir

Kaynak: Davranışlar justanotherpanel.com/api ile karşılaştırılarak doğrulandı ve PanelCompare eşitleme istemcisinde (src/lib/sync/panel-api.ts) uygulandı, 2026-09-11.

Size bir destek talebine mal olan: 401 durumu

JustAnotherPanel hatalı bir anahtara HTTP 401 ve {"error":"Invalid API key"} biçiminde bir JSON gövdesiyle yanıt verir; bu, hata durumu taşıyan yapılandırılmış bir API yanıtıdır. Her 4xx yanıtını “burada API yok” diye yorumlayan bir istemci, asıl sorun anahtarken bir panel sahibine panelinde uç nokta olmadığını söyler. Durumdan bir sonuç çıkarmadan önce gövdeyi ayrıştırın.

Node ile doğru bir istemci neye benzer?

Gerekli hata işlemeyi içeren minimal bir API v2 istemcisi (Node 18+, bağımlılık yok)
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;
}

Bu koddaki üç ayrıntı işin özüdür. Gövde, duruma bakılmadan önce ayrıştırılır. 4xx dalı error anahtarı kontrolünden önce değil, sonra çalışır. Ve her sayısal alan açık bir dönüştürmeden geçer; çünkü rate, min, max, charge, start_count ve remains alanlarının hepsi tırnak içindeki metinler olarak gelir.

Aynı istemci Python ve PHP ile neye benzer?

Python, requests ile
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, curl ile
<?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"];

Bir siparişin hangi parametrelere ihtiyaç duyduğunu nasıl bilirsiniz?

Katalogdaki servisin type alanından; gereklilik yalnızca orada ifade edilir. Her add çağrısı service ve link ister; başka neye ihtiyaç duyduğu tahmin edilemez ve yanlış seti göndermek makul bir varsayılan değil, bir hata üretir. Formu kurmadan önce tipi okuyun.

Servis tipine göre dağıtım
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);
  }
}

Varsayılan dal göründüğünden daha önemlidir. Paneller yeni sipariş tipleri ekler ve sessizce yalnızca miktar içeren bir yapıya düşen bir istemci, bakiye düşüldükten sonra başarısız olan hatalı siparişler verir. Bilinmeyen bir tipte hata fırlatmak, bu hatanın ucuz sürümüdür.

Sipariş durumu nasıl sorgulanmalı?

  1. 1.Çoğul orders parametresiyle, 100’er id’lik gruplar hâlinde toplu sorgulayın. Standart spesifikasyonda multi_status eylemi yoktur ve sipariş başına bir istek, entegrasyonların istek sınırına takılma yoludur.
  2. 2.Kısmen tamamlandı durumunu bir hata olarak değil, birinci sınıf bir sonuç olarak ele alın. Bir kalan (remains) rakamı ve otomatik bir bakiye iadesiyle gelir ve bu piyasadaki en yaygın nihai olmayan sonuçtur.
  3. 3.Sabit, kısa bir aralıkla değil, belirtilen başlama süresi aralığıyla orantılı bir takvimle sorgulayın. Servislerin çoğu bir aralık yayımlar; neredeyse hiçbiri hız yayımlamaz.
  4. 4.Her sipariş id’sinin yanında panelin kimliğini de saklayın. Sipariş id’leri bir panelin içinde benzersizdir, başka hiçbir yerde değil.
  5. 5.Kataloğu düzenli aralıklarla yeniden eşitleyin. Servis id’leri panele özgüdür ve değişebilir; saklanan bir id sessizce farklı bir stoğu göstermeye başlayabilir.
  6. 6.İstek gövdesini asla loglamayın. Anahtar içindedir ve sizi kurtaracak bir son kullanma süresi yoktur.

Durum terimleri az sayıda ve istikrarlıdır: Pending (Beklemede), In progress ya da Processing (İşlemde / İşleniyor), Completed (Tamamlandı), Partial (Kısmen tamamlandı), Canceled (İptal edildi). Bu setin dışındaki her şey, en yakın bilinen değere eşlenmek yerine görünür kılınmalıdır; çünkü beklenmedik bir durum döndüren bir panel genellikle eşlemenin gizleyeceği bir şey söylüyordur.

Spesifikasyon hangi güvenlik kısıtlarını getiriyor?

Anahtar istek gövdesinde imzasız, nonce’suz ve zaman damgasız gider ve spesifikasyon bir son kullanma süresi tanımlamaz. Bu onu uzun ömürlü bir taşıyıcı sır yapar: onu elinde tutan herkes bakiyeyi harcayabilir ve hesaptan sipariş verilmiş bütün linkleri okuyabilir. Zararı sınırlayacak bir tekrar saldırısı koruması ve anahtarı yönetim ekranında yenilemek dışında bir iptal yolu yoktur.

  • Bir tarayıcıdan asla anahtar kabul etmeyin ve asla istemci tarafı kod üzerinden bir anahtarı aktarmayın.
  • İstek gövdesini asla loglamayın ve HTTP istemci kütüphanenizin bunu sizin yerinize loglamadığından emin olun.
  • Anahtarları bir derleme loguna ulaşan ortam değişkenlerinin ve veritabanının dışında tutun.
  • Her üçüncü taraf entegrasyon değişikliğinden ve her personel değişikliğinden sonra yenileyin.
  • Bir anahtar bir ekran görüntüsünde, bir destek talebinde ya da paylaşılan bir belgede göründüğü anda ele geçirildiğini varsayın.

Hızlı cevaplar

SMM panel API’si hatalarda neden 200 döndürüyor?

Çünkü spesifikasyon HTTP durum kodlarını anlamlı biçimde kullanmaz. Hatalar genellikle gövdesinde bir hata nesnesi bulunan HTTP 200 olarak gelir, bazı paneller de hatalı bir anahtara aynı JSON ile 401 döndürür; bu yüzden her istemci hatayı yakalamak için gövdeyi ayrıştırmak zorundadır. Yalnızca res.ok değerine bakmak size hiçbir şey söylemez.

SMM panel API’leri için sandbox ya da test anahtarı var mı?

Hayır. Bu piyasanın hiçbir yerinde sandbox yoktur; ilk başarılı add çağrısı gerçek bir bakiyeye karşı verilmiş gerçek bir sipariştir. Geliştirmeyi, servisin izin verdiği en küçük miktarla ve size ait bir linkte yapın.

Birçok siparişin durumunu aynı anda nasıl sorgularım?

Aynı status eylemini, virgülle ayrılmış en fazla 100 id taşıyan çoğul orders parametresiyle kullanın. Standart spesifikasyonda ayrı bir multi_status eylemi yoktur.

Drip-feed siparişim neden beklediğimin on katını tahsil ediyor?

Çünkü drip-feed siparişinde miktar tekrar başınadır. Teslim edilen ve ücretlendirilen toplam, miktar çarpı tekrar sayısıdır; yani runs=10 ile 1.000 girmek, 10.000 birim sipariş etmek ve 10.000 birimin ücretini ödemek demektir.

Tek bir istemciyi farklı panellerde kullanabilir miyim?

Evet, temel adres ve anahtar değiştirilerek; bütün piyasanın tek bir spesifikasyonu uygulamasının pratik sonucu budur. Taşınmayan şey servis id eşleştirmesidir, çünkü id’ler her panele özgüdür ve başka yere yönlendirilebilir.

Buradaki her rakamın kaynağı ve tarihi belli

Bu piyasada fiyatlar her hafta değişir; bu yüzden ölçüm tarihi olmayan bir rakam süsten ibarettir. Bu rehber bir rakam alıntıladığında kaynağını ve ne zaman kontrol edildiğini belirtir. Bunlardan biri yanlışsa, hakkımızda sayfasındaki düzeltme sürecinde hedeflenen yanıt süresi iki iş günüdür ve düzeltmeler sessizce yamanmaz, tarihli bir notla yayımlanır.