Langsung ke konten

Teknis

Integrasi API panel SMM: panduan yang benar-benar berjalan

Terakhir diperbarui oleh Tim Redaksi PanelCompare, dibaca 5 menit

Apa yang Anda butuhkan sebelum menulis kode?

Akun di panel itu, saldo yang sudah terisi, dan API key dari dashboard panel. Tidak ada sandbox, tidak ada test key, dan tidak ada lingkungan staging di mana pun di pasar ini, sehingga pesanan pertama yang berhasil saat pengembangan adalah pesanan sungguhan yang benar-benar memakan uang dan benar-benar mengirim ke link apa pun yang Anda masukkan. Pakai link milik Anda sendiri yang tidak Anda pedulikan.

Perhitungkan juga ukuran katalog. Katalog sungguhan berisi sekitar 3.000 sampai 8.000 baris, dan action services mengembalikan semuanya dalam satu respons tanpa pembagian halaman, sehingga ini panggilan mahal yang tempatnya di jadwal, bukan di jalur request. Dua panel yang disinkronkan ke indeks PanelCompare masing-masing mengembalikan 5.558 dan 2.196 baris terpetakan (indeks harga PanelCompare, 2026-09-10).

Bagaimana memastikan endpoint-nya ada sebelum autentikasi?

Kirim action services tanpa key sama sekali. Endpoint API v2 sungguhan menjawab dengan error terstruktur, yang membuktikan endpoint itu ada. Respons 404 atau badan HTML membantah klaim API yang dibuat hampir setiap panel di berandanya. Beginilah sebuah panel bisa diverifikasi tanpa pernah memegang kredensialnya.

Mengecek keberadaan endpoint dengan curl, tanpa perlu key
# 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

Sesekali panggilan tanpa key mengembalikan array, bukan error. Itu bukan bug, melainkan katalog terbuka: panel itu memublikasikan seluruh daftar layanannya kepada siapa saja. Cukup jarang sehingga layak dicatat saat Anda menemukannya.

Empat perilaku apa yang merusak integrasi pertama?

Kegagalan yang berasal dari spesifikasi, bukan dari panel tertentu
PerilakuYang dilakukan kode naifYang dilakukan kode yang benar
Error dikembalikan dengan HTTP 200Mengecek res.ok, melihat true, dan memperlakukan objek error sebagai hasilMem-parse badan respons lebih dulu dan mengecek key error sebelum hal lain
Sebagian panel mengembalikan 4xx dengan error JSONLangsung beralih ke path berikutnya hanya berdasarkan status, dan melaporkan key yang salah sebagai API yang tidak adaIkut mem-parse badan respons 4xx; badan dengan key error berarti endpoint-nya berfungsi
Angka datang sebagai stringMembandingkan rate, min, max, atau remains sebagai angka dan salah secara diam-diamMengonversi setiap field angka secara eksplisit di titik parsing
Multi-status adalah parameter jamak, bukan actionMemanggil status sekali per pesanan dan terkena rate limitMengirim sampai 100 id yang dipisahkan koma di parameter orders pada action status yang sama

Sumber: Perilaku diverifikasi terhadap justanotherpanel.com/api dan diterapkan di klien sinkronisasi PanelCompare (src/lib/sync/panel-api.ts), 2026-09-11.

Kasus 401 inilah yang berujung pada tiket dukungan

JustAnotherPanel menjawab key yang salah dengan HTTP 401 dan badan JSON berbentuk {"error":"Invalid API key"} — respons API terstruktur yang kebetulan membawa status error. Klien yang menganggap setiap 4xx sebagai "tidak ada API di sini" akan memberi tahu pemilik panel bahwa panelnya tidak punya endpoint, padahal masalah sebenarnya adalah key-nya. Parse badan responsnya sebelum menarik kesimpulan dari status.

Seperti apa klien yang benar di Node?

Klien API v2 minimal dengan penanganan error yang dibutuhkan (Node 18+, tanpa dependensi)
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;
}

Tiga detail dalam kode itu adalah intinya. Badan respons di-parse sebelum status dilihat. Cabang 4xx dijalankan setelah pengecekan key error, bukan sebelumnya. Dan setiap field angka melewati konversi eksplisit, karena rate, min, max, charge, start_count, dan remains semuanya datang sebagai string berkutip.

Seperti apa klien yang sama di Python dan PHP?

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

Bagaimana tahu parameter apa yang dibutuhkan sebuah pesanan?

Dari field type di baris katalog, satu-satunya tempat syarat itu dinyatakan. Setiap panggilan add butuh service dan link; apa lagi yang dibutuhkan tidak bisa ditebak, dan mengirim kumpulan parameter yang salah menghasilkan error, bukan nilai bawaan yang masuk akal. Baca type-nya sebelum membangun formulirnya.

Memilih parameter berdasarkan tipe baris
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);
  }
}

Cabang default lebih penting daripada kelihatannya. Panel menambah tipe pesanan, dan klien yang diam-diam jatuh ke bentuk quantity saja akan membuat pesanan cacat yang gagal setelah saldo terpotong. Melempar error untuk tipe yang tidak dikenal adalah versi murah dari bug itu.

Bagaimana sebaiknya status pesanan dicek?

  1. 1.Kelompokkan lewat parameter jamak orders, 100 id sekaligus. Spesifikasi bakunya tidak punya action multi_status, dan satu request per pesanan adalah cara integrasi terkena rate limit.
  2. 2.Tangani Partial sebagai hasil yang sah, bukan error. Partial datang dengan angka remains dan pengembalian otomatis ke saldo, dan merupakan hasil tidak final yang paling umum di pasar ini.
  3. 3.Cek dengan jadwal yang sebanding dengan rentang waktu mulai yang diiklankan, bukan dengan interval pendek yang tetap. Sebagian besar baris memublikasikan rentang waktu mulai; hampir tidak ada yang memublikasikan kecepatan.
  4. 4.Simpan identitas panel di samping setiap id pesanan. Id pesanan unik di dalam satu panel dan tidak di tempat lain.
  5. 5.Sinkronkan ulang katalog secara terjadwal. ID layanan bersifat lokal per panel dan bisa berubah, sehingga id yang tersimpan bisa diam-diam menunjuk ke stok lain.
  6. 6.Jangan pernah mencatat badan request ke log. Key-nya ada di dalamnya, dan tidak ada masa kedaluwarsa yang bisa menyelamatkan Anda.

Kosakata statusnya sedikit dan stabil: Pending, In progress atau Processing, Completed, Partial, Canceled. Apa pun di luar kumpulan itu sebaiknya ditampilkan apa adanya, bukan dipetakan ke nilai terdekat yang dikenal, karena panel yang mengembalikan status tak terduga biasanya sedang memberi tahu Anda sesuatu yang akan tersembunyi oleh pemetaan itu.

Batasan keamanan apa yang ditetapkan spesifikasinya?

Key dikirim di badan request tanpa penandatanganan, tanpa nonce, dan tanpa stempel waktu, dan spesifikasinya tidak menetapkan masa kedaluwarsa. Itu menjadikannya rahasia bearer berumur panjang: siapa pun yang memegangnya bisa membelanjakan saldo dan membaca setiap link yang pernah dipesan dari akun itu. Tidak ada perlindungan replay untuk membatasi kerusakan dan tidak ada pencabutan selain mengganti key di dashboard.

  • Jangan pernah menerima key dari browser dan jangan pernah meneruskannya lewat kode sisi klien.
  • Jangan pernah mencatat badan request ke log, dan pastikan library klien HTTP Anda tidak mencatatnya untuk Anda.
  • Jauhkan key dari variabel lingkungan yang sampai ke log build, dan dari database.
  • Ganti key setelah setiap perubahan integrasi pihak ketiga, dan setelah setiap pergantian staf.
  • Anggap key sudah bocor begitu muncul di tangkapan layar, tiket, atau dokumen bersama.

Jawaban cepat

Mengapa API panel SMM mengembalikan 200 saat terjadi error?

Karena spesifikasinya tidak memakai kode status HTTP secara bermakna. Kegagalan biasanya kembali sebagai HTTP 200 dengan objek error di badannya, dan sebagian panel menjawab key yang salah dengan 401 dan JSON yang sama, sehingga setiap klien harus mem-parse badan respons untuk mendeteksi kegagalan. Mengecek res.ok saja tidak memberi tahu Anda apa pun.

Apakah ada sandbox atau test key untuk API panel SMM?

Tidak. Tidak ada sandbox di mana pun di pasar ini, sehingga panggilan add pertama yang berhasil adalah pesanan sungguhan yang memotong saldo sungguhan. Kembangkan dengan jumlah terkecil yang diizinkan baris itu, pada link milik Anda sendiri.

Bagaimana cara mengecek status banyak pesanan sekaligus?

Pakai action status yang sama dengan parameter jamak orders yang berisi sampai 100 id yang dipisahkan koma. Spesifikasi bakunya tidak punya action multi_status tersendiri.

Mengapa pesanan drip-feed saya menagih sepuluh kali lipat dari perkiraan?

Karena jumlah di pesanan drip-feed berlaku per run. Total yang terkirim dan ditagih sama dengan jumlah dikali runs, sehingga 1.000 dengan runs=10 berarti memesan dan membayar 10.000 unit.

Bisakah satu klien dipakai di panel yang berbeda-beda?

Bisa, dengan mengganti base URL dan key; itulah konsekuensi praktis dari seluruh pasar yang menerapkan satu spesifikasi. Yang tidak terbawa adalah pemetaan id layanan, karena id bersifat lokal di setiap panel dan bisa dialihkan.

Setiap angka di sini bersumber dan bertanggal

Harga di pasar ini bergerak setiap minggu, jadi angka tanpa tanggal pengambilan hanyalah hiasan. Setiap kali panduan ini mengutip angka, sumber dan tanggal pemeriksaannya disebutkan. Jika salah satunya keliru, proses koreksi di halaman tentang kami menargetkan balasan dalam dua hari kerja, dan koreksi dipublikasikan dengan catatan bertanggal, bukan ditambal diam-diam.