الجوانب التقنية
ربط API لوحة SMM: شرح عملي خطوة بخطوة
آخر تحديث: ، من إعداد فريق تحرير PanelCompare، مدة القراءة 5 دقائق
ماذا تحتاج قبل كتابة أي شيفرة؟
حسابًا في اللوحة، ورصيدًا مشحونًا، ومفتاح API من لوحة التحكم في اللوحة. لا توجد بيئة تجريبية (sandbox) ولا مفتاح اختبار ولا بيئة ما قبل الإنتاج في أي مكان من هذه السوق، فأول طلب ناجح أثناء التطوير طلب حقيقي يكلّف مالًا حقيقيًا ويُسلَّم فعلًا إلى أي رابط تضعه فيه. استخدم رابطًا تملكه ولا يهمك.
وضع في حسابك أيضًا حجم قائمة الخدمات. قوائم الخدمات الفعلية تمتد من نحو 3,000 إلى 8,000 عرض، وإجراء services يعيدها كلها في رد واحد غير مقسّم إلى صفحات، فهو استدعاء مكلف مكانه جدول زمني لا مسار طلب مباشر. واللوحتان المُزامَنتان في مؤشر PanelCompare تعيدان 5,558 و2,196 عرضًا مطابقًا على التوالي (مؤشر أسعار PanelCompare، 2026-09-10).
كيف تتأكد من وجود النقطة قبل المصادقة؟
أرسل إجراء services دون أي مفتاح. نقطة API v2 الحقيقية تجيب بخطأ منظّم، وهذا يثبت وجودها. أما ردّ 404 أو جسم HTML فينقض ادعاء API الذي تكتبه كل لوحة تقريبًا في صفحتها الرئيسية. وبهذه الطريقة يمكن التحقق من لوحة دون امتلاك أي بيانات دخول لها.
# 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أحيانًا يعيد الاستدعاء بلا مفتاح مصفوفة بدل خطأ. هذا ليس خللًا، بل قائمة خدمات مفتوحة: اللوحة تنشر قائمة خدماتها كاملة لأي أحد. وهو نادر بما يكفي ليستحق التسجيل حين تصادفه.
ما السلوكيات الأربعة التي تُفشل أول ربط برمجي؟
| السلوك | ما تفعله الشيفرة الساذجة | ما تفعله الشيفرة الصحيحة |
|---|---|---|
| الأخطاء تعود برمز HTTP 200 | تفحص res.ok، فتجده true، وتعامل كائن الخطأ على أنه نتيجة | تحلّل الجسم أولًا وتبحث عن مفتاح error قبل أي شيء آخر |
| بعض اللوحات تعيد 4xx مع خطأ بصيغة JSON | تنتقل إلى المسار التالي بناءً على الرمز وحده، وتبلّغ عن مفتاح خاطئ على أنه API غير موجود | تحلّل جسم 4xx أيضًا؛ فالجسم الذي فيه مفتاح error يعني نقطة عاملة |
| الأرقام تصل نصوصًا | تقارن rate أو min أو max أو remains رقميًا فتعطي نتائج خاطئة بصمت | تحوّل كل خانة رقمية صراحةً عند حدّ التحليل |
| الاستعلام عن عدة حالات معامل بصيغة الجمع، لا إجراء | تستدعي status مرة لكل طلب فتصطدم بحدود معدل الاستدعاء | ترسل حتى 100 رقم مفصولة بفواصل في معامل orders ضمن إجراء status نفسه |
المصدر: السلوك تم التحقق منه على justanotherpanel.com/api ومطبّق في عميل المزامنة لدى PanelCompare (src/lib/sync/panel-api.ts)، 2026-09-11.
حالة 401 هي التي تكلّفك تذكرة دعم
JustAnotherPanel تجيب عن المفتاح الخاطئ بـHTTP 401 وجسم JSON بصيغة {"error":"Invalid API key"}، أي ردّ API منظّم تصادف أنه يحمل حالة خطأ. والعميل الذي يعامل أي 4xx على أنه «لا API هنا» سيخبر صاحب لوحة أن لوحته بلا نقطة، بينما المشكلة الحقيقية في المفتاح. حلّل الجسم قبل أن تستنتج شيئًا من رمز الحالة.
كيف يبدو العميل الصحيح في Node؟
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;
}ثلاث تفاصيل في هذه الشيفرة هي بيت القصيد. الجسم يُحلَّل قبل الرجوع إلى رمز الحالة. وفرع 4xx يأتي بعد فحص مفتاح error لا قبله. وكل خانة رقمية تمر بتحويل صريح، لأن rate وmin وmax وcharge وstart_count وremains كلها تصل نصوصًا بين علامتي تنصيص.
كيف يبدو العميل نفسه في Python وPHP؟
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
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"];كيف تعرف المعاملات التي يحتاجها الطلب؟
من خانة type في عرض الخدمة، وهي المكان الوحيد الذي يُعبَّر فيه عن المتطلبات. كل استدعاء add يحتاج service وlink؛ أما ما يحتاجه غير ذلك فلا يمكن تخمينه، وإرسال مجموعة خاطئة ينتج خطأً لا قيمة افتراضية معقولة. اقرأ النوع قبل بناء النموذج.
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);
}
}الفرع الافتراضي أهم مما يبدو. اللوحات تضيف أنواع طلبات، والعميل الذي ينحدر بصمت إلى شكل «الكمية فقط» سيضع طلبات مشوّهة تفشل بعد خصم المبلغ من الرصيد. ورمي خطأ عند نوع غير معروف هو النسخة الرخيصة من هذا الخلل.
كيف ينبغي الاستعلام عن حالة الطلبات؟
- 1.اجمع الطلبات عبر معامل orders بصيغة الجمع، 100 رقم في كل مرة. لا يوجد إجراء multi_status في المواصفة المعيارية، وطلب واحد لكل أمر شراء هو ما يوقع الربط في حدود معدل الاستدعاء.
- 2.تعامل مع حالة Partial على أنها نتيجة أصيلة لا خطأ. فهي تأتي مع رقم remains وإعادة تلقائية للقيمة إلى الرصيد، وهي أشيع نتيجة غير نهائية في هذه السوق.
- 3.استعلم وفق جدول يتناسب مع مدة وقت البدء المعلنة، لا بفاصل قصير ثابت. معظم العروض تنشر مدة لوقت البدء، ولا يكاد أي منها ينشر سرعة.
- 4.احفظ هوية اللوحة إلى جانب كل رقم طلب. أرقام الطلبات فريدة داخل اللوحة الواحدة ولا مكان غيرها.
- 5.أعد مزامنة قائمة الخدمات وفق جدول. أرقام الخدمات خاصة بكل لوحة وقابلة للتغيير، فقد يبدأ رقم محفوظ بالإشارة بصمت إلى مخزون مختلف.
- 6.لا تسجّل جسم الطلب في السجلات أبدًا. المفتاح بداخله، ولا انتهاء صلاحية ينقذك.
مفردات الحالات قليلة وثابتة: Pending، ثم In progress أو Processing، ثم Completed أو Partial أو Canceled. وأي قيمة خارج هذه المجموعة ينبغي إظهارها لا ربطها بأقرب قيمة معروفة، لأن اللوحة التي تعيد حالة غير متوقعة تخبرك عادةً بشيء سيخفيه الربط.
ما القيود الأمنية التي تفرضها المواصفة؟
المفتاح ينتقل داخل جسم الطلب بلا توقيع ولا nonce ولا طابع زمني، والمواصفة لا تحدد أي انتهاء لصلاحيته. وهذا يجعله سرًا طويل الأمد لحامله: أي شخص يملكه يستطيع صرف الرصيد وقراءة كل رابط طُلب عليه في الحساب. ولا حماية من إعادة الإرسال تحدّ من الضرر، ولا إلغاء سوى تغيير المفتاح من لوحة التحكم.
- لا تقبل مفتاحًا من المتصفح أبدًا، ولا تمرّره عبر شيفرة تعمل في جهة العميل.
- لا تسجّل جسم الطلب في السجلات أبدًا، وتحقق من أن مكتبة HTTP التي تستخدمها لا تسجّله نيابة عنك.
- أبقِ المفاتيح خارج متغيرات البيئة التي تصل إلى سجل البناء، وخارج قاعدة البيانات.
- غيّر المفاتيح بعد كل تغيير في ربط مع طرف خارجي، وبعد أي تغيير في الموظفين.
- افترض الاختراق لحظة ظهور المفتاح في لقطة شاشة أو تذكرة دعم أو مستند مشترك.
إجابات سريعة
لماذا يعيد API لوحات SMM الرمز 200 عند الأخطاء؟
لأن المواصفة لا تستخدم رموز حالة HTTP استخدامًا ذا معنى. الأخطاء تعود عادةً برمز HTTP 200 وفي الجسم كائن خطأ، وبعض اللوحات تردّ على المفتاح الخاطئ بـ401 مع صيغة JSON نفسها، فعلى كل عميل أن يحلّل الجسم ليكتشف الفشل. وفحص res.ok وحده لا يخبرك بشيء.
هل توجد بيئة تجريبية (Sandbox) أو مفتاح اختبار لـAPI لوحات SMM؟
لا. لا توجد بيئة تجريبية في أي مكان من هذه السوق، فأول استدعاء add ناجح طلب حقيقي على رصيد حقيقي. طوّر على أصغر كمية يسمح بها العرض، وعلى رابط تملكه.
كيف أستعلم عن حالة طلبات كثيرة دفعة واحدة؟
استخدم إجراء status نفسه مع معامل orders بصيغة الجمع، يحمل حتى 100 رقم مفصولة بفواصل. لا يوجد إجراء multi_status مستقل في المواصفة المعيارية.
لماذا يخصم طلب التسليم التدريجي عشرة أضعاف ما توقعته؟
لأن الكمية في طلب التسليم التدريجي لكل دفعة. الإجمالي المسلَّم والمخصوم يساوي الكمية مضروبة في runs، فإدخال 1,000 مع runs=10 يعني طلب 10,000 وحدة ودفع ثمنها.
هل يمكنني استخدام عميل برمجي واحد مع لوحات مختلفة؟
نعم، بتغيير العنوان الأساسي والمفتاح، وهذه النتيجة العملية لتطبيق السوق كلها مواصفة واحدة. ما لا ينتقل هو ربط أرقام الخدمات، لأن الأرقام خاصة بكل لوحة ويمكن تحويلها إلى مزوّد آخر.
كل رقم هنا منسوب إلى مصدره ومؤرخ
الأسعار في هذه السوق تتغيّر أسبوعيًا، فالرقم الذي لا تاريخ لرصده رقم للزينة. وحيث يذكر هذا الدليل رقمًا، يسمّي مصدره ومتى تحققنا منه. وإن كان أحدها خاطئًا، فإجراء التصحيح المشروح في صفحة «من نحن» يلتزم بالرد خلال يومي عمل، وتُنشر التصحيحات مع ملاحظة مؤرخة بدل أن تُعدَّل بصمت.