توثيق API
كل ما تقدمه اللوحة متاح عبر واجهتين برمجيتين (API) منفصلتين. تعمل الاثنتان بالحساب نفسه والرصيد نفسه وقائمة الخدمات نفسها، والفرق بينهما في الصيغة والإمكانات.
كل ما تقدمه اللوحة متاح عبر واجهتين برمجيتين (API) منفصلتين. تعمل الاثنتان بالحساب نفسه والرصيد نفسه وقائمة الخدمات نفسها، والفرق بينهما في الصيغة والإمكانات.
لماذا واجهتان برمجيتان؟
يعتمد القطاع كله على API الموزّعين الكلاسيكي (v2): تُرسل إليه البيانات كنموذج (form) عبر نقطة اتصال واحدة، ويرد دائمًا برمز HTTP 200. وهذه بالضبط الصيغة التي تتوقعها برامج اللوحات الجاهزة، لذلك يبقى كما هو. أما المطورون الذين يبنون أنظمتهم الخاصة فكانوا يصطدمون بحدوده مرة بعد مرة: لا يمكن التمييز بين الأخطاء، وقائمة الخدمات تصل كتلة واحدة، ومعرفة حالة الطلب تتطلب استعلامًا متكررًا بلا نهاية. من أجلهم كُتب v3.
مقارنة
| الميزة | القديم (v2) | الجديد (v3) |
|---|---|---|
| البنية | نقطة اتصال واحدة، وإرسال نموذج، والمُعامل action | REST قائم على الموارد، وجسم طلب بصيغة JSON |
| رمز حالة HTTP | دائمًا 200، حتى عند الفشل | رموز حقيقية (400، 401، 402، 404، 409، 429، 502) |
| الأخطاء | نص حر | type + code ثابت + رسالة مترجمة + param + doc_url |
| حالة الطلب | نص مترجم فقط | قيمة آلية ثابتة مع تسمية عرض منفصلة |
| وصف الخدمة | لا يوجد | وصف بـ 16 لغة، ومتوسط الوقت، والمنصة، والفئة |
| حقول الطلب | تُستنتج من اسم النوع | كل خدمة تنشر مخطط حقولها الخاص |
| وحدة التسعير | غير مذكورة (مصدر لخطأ بمقدار 1000 ضعف في الباقات) | صريحة: per_1000 أو per_order |
| قائمة الخدمات | كل الخدمات في استجابة واحدة | عوامل تصفية مع ترقيم صفحات بالمؤشر (cursor) |
| الحماية من التكرار | لا توجد | Idempotency-Key |
| تحديثات الحالة | استعلام متواصل | Webhooks موقّعة أو تدفق أحداث |
| المخطط | لا يوجد | OpenAPI 3.1 |
| اللغات | الإنجليزية والتركية (بعنواني URL منفصلين) | 16 لغة (عبر ترويسة أو مُعامل) |
| جودة الخدمة | لا توجد | درجة ومستوى ثقة وأدلة لكل خدمة، مع قائمة مختصرة مرتبة |
أيهما أستخدم؟
اختر API القديم إذا كنت تستخدم برنامج لوحة جاهزًا أو بوتًا أو لوحة موزّعين. أغلب هذه البرامج لا تطلب منك سوى تغيير عنوان API والمفتاح، ثم تبدأ العمل خلال دقائق.
اختر v3 إذا كنت تكتب تطبيقك الخاص أو متجرك أو أدوات الأتمتة لديك. معالجة الأخطاء والحماية من التكرار والإشعارات مدمجة فيه، ويمكنك توليد نموذج الطلب مباشرة من مخطط الخدمة.
خطوات البدء
- 1أنشئ مفتاح API من تبويب «المفاتيح».
- 2اجلب قائمة الخدمات، واقرأ المعرف (id) ومخطط الحقول للخدمة التي تحتاجها.
- 3تحقق من الطلب أولًا عبر preview، ثم أنشئه.
- 4سجّل Webhook أو اقرأ تدفق الأحداث لتتبع تغيرات الحالة.
واجهة REST مصممة للمطورين الذين يبنون أنظمتهم الخاصة: مسارات قائمة على الموارد، ورموز حالة HTTP حقيقية، وأخطاء قابلة للقراءة آليًا، وإشعارات موقّعة.
عنوان URL الأساسي
يُضاف كل مسار إلى نهاية هذا العنوان. رقم الإصدار جزء من المسار: إذا احتجنا يومًا إلى تغيير يكسر التوافق مع الإصدار الحالي، ننشر مسارًا جديدًا (v4) ويواصل هذا المسار العمل دون أي تعديل. وتحمل كل استجابة ترويسة X-Api-Version وفيها تاريخ إصدار مواصفات الواجهة.
https://panelfollows.com/api/v3المصادقة
أرسل مفتاح API كرمز Bearer في ترويسة Authorization. ونقبل ترويسة X-Api-Key بديلًا عنها.
GET https://panelfollows.com/api/v3/account
Authorization: Bearer pf_live_...مفتاحك القديم يعمل أيضًا على v3، فيمكنك التجربة فورًا. لكن استخدم مفتاح v3 في بيئة الإنتاج: يمكنك تسميته وإبطاله منفردًا، ولا يُخزن أبدًا كنص صريح.
بداية سريعة
curl https://panelfollows.com/api/v3/services?limit=5 \
-H "Authorization: Bearer YOUR_API_KEY"اللغة
اختر لغة الاستجابة عبر ترويسة Accept-Language أو المُعامل ?lang=، وعند إرسال الاثنين تكون الأولوية للمُعامل. تصلك بهذه اللغة أسماء الخدمات وأوصافها، وأسماء الفئات، وتسميات حالات الطلب، وتسميات حقول الطلب، ورسائل الخطأ.
القيم الآلية لا تتغير مع اللغة أبدًا: error.code وorder.status وservice.type وcurrency ثابتة دائمًا. اعتمد عليها في منطق برنامجك، واعرض النص لمستخدميك.
Accept-Language: tr
# veya
GET https://panelfollows.com/api/v3/services?lang=trصيغة الطلب والاستجابة
جسم الطلب بصيغة JSON (application/json)، ونقبل أيضًا form-urlencoded للاختبارات السريعة. الاستجابات بصيغة JSON: المورد الواحد كائن عادي، والقوائم تأتي داخل غلاف يضم data وhas_more وnext_cursor. ويحمل كل كائن حقل object يذكر نوعه.
المبالغ سلاسل نصية عشرية ("1.2340")، وليست أعدادًا بفاصلة عائمة (float). حوّلها في نظامك إلى نوع decimal حتى لا تضيع أي كسور. العملة هي USD.
الطوابع الزمنية بصيغة RFC 3339 (2026-08-21T00:24:45.255Z).
الأخطاء
تعيد الطلبات الفاشلة رمز حالة HTTP حقيقيًا، وجسم استجابة يحمل كائن error واحدًا. اعتمد على error.code في منطق التعامل مع الأخطاء: هذه القيمة ثابتة ولا تتغير مع اللغة أبدًا.
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: req_0c858d8af7f65eca001b2f5a
{
"object": "error",
"error": {
"type": "invalid_request_error",
"code": "quantity_out_of_range",
"message": "Miktar, bu servisin izin verdiği aralığın dışında.",
"param": "quantity",
"doc_url": "https://panelfollows.com/api-docs#error-quantity_out_of_range",
"request_id": "req_0c858d8af7f65eca001b2f5a"
}
}| type | الفئة العامة للخطأ: هل يمكن إعادة المحاولة، وهل الخطأ من جانبك. |
| code | قيمة آلية ثابتة. اعتمد عليها في منطق برنامجك. |
| message | نص مقروء باللغة التي اخترتها. |
| param | اسم الحقل الذي سبّب الخطأ، إن وُجد. |
| doc_url | رابط إلى القسم المعني بالضبط في هذا التوثيق. |
| request_id | المرجع الوحيد الذي تحتاج إلى ذكره عند التواصل مع الدعم. |
حدود الطلبات
الحد 600 طلب في الدقيقة لكل مفتاح، و900 طلب في الدقيقة لكل عنوان IP. تحمل كل استجابة الترويسات RateLimit-Limit وRateLimit-Remaining وRateLimit-Reset، لتخفض وتيرة طلباتك قبل أن تصل إلى الحد. وعند تجاوزه تصلك الاستجابة 429 مع ترويسة Retry-After.
ترقيم الصفحات
القوائم مقسمة إلى صفحات بالمؤشر (cursor). أرسل limit لتحديد حجم الصفحة (500 كحد أقصى)، وstarting_after مع معرف آخر عنصر في الصفحة السابقة. واصل حتى تصبح قيمة has_more هي false، ويعطيك next_cursor المؤشر للاستدعاء التالي. قيمة limit الخارجة عن النطاق لا نقلصها بصمت، بل نعيد خطأ: فالتقليص الصامت يجعل تطبيق العميل يظن أنه جلب كل شيء.
الحماية من التكرار (Idempotency-Key)
أضف ترويسة Idempotency-Key بقيمة عشوائية عند إنشاء الطلب. إذا انقطع الاتصال وأعدت المحاولة بالمفتاح نفسه، فلن يُنشأ طلب ثانٍ: نعيد إليك الاستجابة الأولى نفسها مع ترويسة Idempotent-Replay: true. ونحتفظ بالسجلات لمدة 24 ساعة.
إرسال المفتاح نفسه مع جسم طلب مختلف يعيد الخطأ 409 idempotency_key_reuse. وهذا يعني في أغلب الحالات أن في طريقة توليد المفاتيح لديك خللًا. الطلب الفاشل لا يستهلك المفتاح: أصلح المشكلة وأعد المحاولة بالمفتاح نفسه.
إنشاء طلب
curl -X POST https://panelfollows.com/api/v3/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ea3a-4b1c-9c1e-2b0d5c6a7e91" \
-d '{
"service": 1234,
"link": "https://instagram.com/username",
"quantity": 1000
}'مخطط حقول الخدمة
تنشر كل خدمة الحقول التي تحتاجها في المصفوفة fields: الاسم، والنوع، وهل الحقل إلزامي، وحدوده، وتسمية ووصف بلغتك. وعندما تكون قيمة determines_quantity في حقل ما هي true، تُحسب الكمية من عدد الأسطر فيه.
// Servisin kendi alan şemasından formu OTOMATİK üretmek:
// hiçbir servis tipini koda gömmeniz gerekmez.
const res = await fetch("https://panelfollows.com/api/v3/services/1234", {
headers: { Authorization: "Bearer YOUR_API_KEY", "Accept-Language": "tr" },
});
const service = await res.json();
for (const field of service.fields) {
renderInput({
name: field.name,
label: field.label, // kullanıcının dilinde
hint: field.description, // kullanıcının dilinde
required: field.required,
type: field.type, // url | integer | string | text_lines
min: field.min,
max: field.max,
// true ise miktarı bu alanın satır sayısı belirler
countsLines: field.determines_quantity === true,
});
}كائن الخدمة
{
"object": "service",
"id": 1234,
"name": "Instagram Takipçi | Türk | 30 gün telafi",
"description": "Gerçek hesaplardan Türk takipçi. Başlangıç 0-1 saat.",
"type": "default",
"platform": "instagram",
"category": { "slug": "instagram-takipci", "name": "Instagram Takipçi" },
"pricing": {
"rate": "1.2340",
"currency": "USD",
"unit": "per_1000",
"unit_note": "Fiyat 1000 adet içindir."
},
"limits": { "min": 100, "max": 100000 },
"features": { "refill": true, "cancel": false, "dripfeed": true },
"average_time_seconds": 4320,
"fields": [
{
"name": "link",
"type": "url",
"required": true,
"label": "Bağlantı",
"description": "Gönderimin yapılacağı profilin herkese açık adresi."
},
{
"name": "quantity",
"type": "integer",
"required": true,
"label": "Miktar",
"description": "Kaç adet gönderileceği.",
"min": 100,
"max": 100000
}
],
"is_active": true,
"updated_at": "2026-08-20T09:15:00.000Z"
}معاينة الطلب (تشغيل تجريبي)
يتحقق POST /orders/preview من الطلب ويحسب تكلفته دون أن ينشئه. اعرض السعر على عميلك، وتأكد مسبقًا أن رصيدك يغطيه. لا نخصم أي مبلغ، ولا نتواصل مع أي مزوّد.
الطلبات الجماعية
يقبل POST /orders/batch حتى 50 طلبًا في استدعاء واحد. نعالج العناصر بالترتيب، ويعيد كل عنصر نتيجته الخاصة: إذا فشل أحدها تُنشأ البقية رغم ذلك، وترى بالضبط أيها فشل ولماذا.
تضمين الكائنات المرتبطة
مرر include=service إلى نقاط الاتصال الخاصة بالطلبات ليأتي كائن الخدمة مضمنًا في الاستجابة، فتوفر على نفسك طلبًا ثانيًا.
جودة الخدمة وقائمة أفضل الخدمات
تقيس اللوحة كل خدمة كل ساعة: كيف انتهت طلباتنا نحن عليها (مكتملة، ملغاة، عالقة، مرفوضة)، وكم مرة طلب العملاء تعويضًا أو فتحوا تذكرة، وكم استغرق التسليم فعليًا، وهل ما زال المصدر يدرجها. يحول GET /services/top هذه القياسات إلى قائمة مختصرة مرتبة، ويرفق include=quality التقرير نفسه بأي كائن خدمة.
الدرجة (0-100) مزيج مرجح من خمسة مكونات: الموثوقية (42%، وهي نسبة النجاح المقدرة)، والرضا (14%، من معدل الشكاوى)، والسرعة (28%، بمقياس لوغاريتمي لوقت التسليم الفعلي: 5 دقائق تنال الدرجة كاملة، و48 ساعة تنال صفرًا)، ودرجة محرك مراقبة الصحة نفسه (8%)، وحجم الأدلة (8%). وتضيف مكافآت صغيرة نقاطًا لضمان التعويض، وللمصدر الموثوق، ولمدة بقاء الخدمة في القائمة. الخدمات قيد المراجعة، أو التي نبّه إليها محرك مراقبة الصحة، أو القادمة من مصدر في فترة اختبار، تحصل على درجة لكنها لا تدخل الترتيب أبدًا.
يمكنك تصفية النتائج حسب المنصة، أو slug الفئة، أو الرف (followers وlikes وviews... وهي مشتركة بين المنصات)، وترتيبها حسب score أو speed أو reliability أو price أو orders، واستخدام group_by=category (أو platform) مع per_group لتحصل على قائمة مختصرة لكل فئة في استدعاء واحد: وهذا بالضبط ما يحتاجه المتجر ليميز خدمة «موصى بها» في كل فئة.
# Instagram takipçi kategorisinde en iyi 5 servis
curl "https://panelfollows.com/api/v3/services/top?category=instagram-followers&limit=5" -H "Authorization: Bearer YOUR_API_KEY"
# Her kategori için kanıtlı en iyi 3 servis (tek istek)
curl "https://panelfollows.com/api/v3/services/top?group_by=category&per_group=3&min_confidence=medium" -H "Authorization: Bearer YOUR_API_KEY"| score / grade | من 0 إلى 100، والتقدير A (85+)، أو B (70+)، أو C (55+)، أو D. |
| confidence | none أو low أو medium أو high: عدد طلباتنا الخاصة التي تستند إليها الدرجة. |
| badges | proven (5 طلبات فأكثر، والحد الأدنى المقدر لنسبة النجاح 60% فأكثر)، وpopular (20 طلبًا فأكثر)، وfast (تسليم خلال ساعة واحدة)، وtrusted_source، وnew (أقل من 14 يومًا). |
| components | reliability وsatisfaction وspeed وhealth وevidence، وقيمة كل منها بين 0 و1. |
| evidence.basis | service_orders (طلبات الخدمة نفسها)، أو peer_services (بدأت من خدمات المصدر الأخرى)، أو none. |
| evidence.delivery_source | measured (الوسيط الذي قسناه على 3 طلبات مكتملة أو أكثر)، أو claimed (الوقت الذي يعلنه المصدر، وتُطبق عليه عقوبة وسقف أعلى). |
| measured_at | آخر مرة قاس فيها محرك مراقبة الصحة الخدمة، ويجري تحديث الدرجات كل ساعة. |
مرجع نقاط الاتصال
| الطريقة | نقطة الاتصال | الوصف |
|---|---|---|
| GET | /api/v3 | Discovery document: version, endpoints, limits and event types. |
| GET | /api/v3/openapi.json | OpenAPI 3.1 schema for this API. |
| GET | /api/v3/account | Account balance, currency and current rate-limit window. |
| PATCH | /api/v3/account | Set the low balance alert threshold that triggers account.low_balance. |
| GET | /api/v3/services | List services with filters and cursor pagination. |
| GET | /api/v3/services/top | Best services ranked by the quality score; optionally one shortlist per category or platform. |
| GET | /api/v3/services/{id} | Retrieve one service, including its order field schema. |
| GET | /api/v3/categories | List categories with platform, shelf and active service counts. |
| GET | /api/v3/platforms | List platform keys usable as the ?platform= filter, with counts. |
| POST | /api/v3/orders | Create an order. Supports the Idempotency-Key header. |
| POST | /api/v3/orders/preview | Validate an order and compute its charge without creating it. |
| POST | /api/v3/orders/batch | Create up to 50 orders in one call; each item reports its own result. |
| GET | /api/v3/orders | List your orders, newest first. |
| GET | /api/v3/orders/{id} | Retrieve one order. |
| POST | /api/v3/orders/{id}/cancel | Request cancellation. Only for services whose features.cancel is true. |
| POST | /api/v3/orders/{id}/refill | Request a refill for a completed order. |
| GET | /api/v3/refills | List your refill requests, newest first. |
| GET | /api/v3/refills/{id} | Retrieve one refill; refreshes its status from the provider. |
| GET | /api/v3/events | Read your event stream oldest-first; the polling alternative to webhooks. |
| GET | /api/v3/webhooks | List your webhook endpoints. |
| POST | /api/v3/webhooks | Register a webhook endpoint. The signing secret is returned once. |
| GET | /api/v3/webhooks/{id} | Retrieve one webhook endpoint. |
| PATCH | /api/v3/webhooks/{id} | Update a webhook endpoint's url, events, description or active state. |
| DELETE | /api/v3/webhooks/{id} | Delete a webhook endpoint and its delivery log. |
| POST | /api/v3/webhooks/{id}/test | Send a test event to this endpoint, ignoring its event filter. |
| POST | /api/v3/webhooks/{id}/rotate_secret | Generate a new signing secret. The old one stops working immediately. |
| GET | /api/v3/webhooks/{id}/deliveries | Delivery log for one endpoint: attempts, response codes and errors. |
مخطط OpenAPI
تعريف قابل للقراءة آليًا لكل نقاط الاتصال. استخدم هذا الملف مع أداة توليد كود العميل (openapi-generator أو Kiota) أو مع Postman، لتحصل على مكتبة عميل (client) جاهزة بلغة البرمجة التي تستخدمها.
https://panelfollows.com/api/v3/openapi.jsonالأسئلة الشائعة
هل يعمل مفتاحي القديم على v3؟
نعم. لا تحتاج إلى مفتاح جديد لتجربته. ومع ذلك انتقل إلى مفتاح v3 في بيئة الإنتاج: يمكن إبطاله، ولا يُخزن كنص صريح.
لماذا تأتي الأسعار كسلاسل نصية؟
الأعداد ذات الفاصلة العائمة (float) تفقد أجزاء من الكسور في المبالغ العشرية. حين نعيد القيم كسلاسل نصية وتحولها أنت إلى نوع decimal، تتخلص من فروق التقريب هذه كلها.
لماذا يهم الحقل unit؟
معظم الخدمات مسعرة لكل 1000 وحدة (per_1000)، أما خدمات الباقات فتُباع كعنصر واحد (per_order) ويغطي السعر الباقة كاملة. التكاملات التي تجاهلت هذا الفرق أخطأت في حساب أسعار الباقات بمقدار 1000 ضعف.
أُنشئ طلبي لكنه بقي في حالة pending. ماذا حدث؟
ربما تأخر تسليم الطلب إلى المزوّد. رصيدك محجوز والطلب لم يضع، وفريقنا يعيد إرساله تلقائيًا. ويشير الحقل processing_delayed إلى هذه الحالة.
هل تدعم كل الخدمات الإلغاء والتعويض؟
لا. راجع features.cancel وfeatures.refill في كائن الخدمة. استدعاء نقطة الاتصال لخدمة لا تدعم ذلك يعيد الخطأ 400.
هل يمكنني استخدام الواجهتين معًا؟
نعم. الحساب نفسه، والرصيد نفسه، والطلبات نفسها. الطلب الذي تقدمه عبر v2 يمكنك قراءته عبر v3.
هذا هو API القياسي للموزّعين، المعتمد في القطاع كله. وهي الصيغة التي تتوقعها برامج اللوحات الجاهزة.
نقطة الاتصال
POST https://panelfollows.com/api/v2
POST https://panelfollows.com/api/v2/trالمصادقة
يحمل كل طلب المُعامل key. حافظ على سرية مفتاحك، وأعد توليده فورًا إذا تسرب.
صيغة الطلب والاستجابة
تُرسل الطلبات بطريقة POST كنموذج (application/x-www-form-urlencoded)، والاستجابات بصيغة JSON. وحتى عند الفشل تعود الاستجابة برمز HTTP 200، مع { "error": "..." } في جسمها.
مدعوم أيضًا، مع أنه لم يُوثق من قبل: يمكنك الاستدعاء بطريقة GET، ويمكنك إرسال جسم الطلب بصيغة application/json.
حدود الطلبات
الحد 240 طلبًا في الدقيقة لكل مفتاح، و300 طلب في الدقيقة لكل عنوان IP. نرفض الطلبات الزائدة بالرمز 429.
الإجراءات والمُعاملات
| action | المُعامل | الوصف |
|---|---|---|
| services | key, action | يعرض كل الخدمات النشطة (المعرف، والاسم، والفئة، والسعر، والحد الأدنى والأقصى، والتعويض، والإلغاء، والتنقيط). |
| add | key, action, service, link, quantity[, runs, interval, comments, username, posts, min, max] | ينشئ طلبًا. service هو معرف الخدمة في قائمة الخدمات. أضف runs وinterval للتنقيط (Drip-feed)، والحقول المناسبة للأنواع الخاصة. |
| status | key, action, order | orders | حالة الطلب. استخدم order لطلب واحد، أو orders مع قائمة معرفات مفصولة بفواصل (,) لعدة طلبات. |
| balance | key, action | رصيد الحساب وعملته. |
| refill | key, action, order | orders | ينشئ طلب تعويض يُرسل مباشرة إلى المزوّد. |
| refill_status | key, action, refill | refills | يستعلم عن حالة التعويض. |
| cancel | key, action, orders | يلغي الطلبات. يعمل فقط مع الخدمات التي يدعم مزوّدها الإلغاء. |
مثال
curl -X POST https://panelfollows.com/api/v2 \
-d "key=YOUR_API_KEY" \
-d "action=add" \
-d "service=1234" \
-d "link=https://instagram.com/username" \
-d "quantity=1000"
# Yanıt: { "order": 23501 }الاستجابات باللغة التركية
أضف /tr إلى نهاية عنوان URL لتصلك أسماء الخدمات والفئات وحالات الطلبات ورسائل الخطأ باللغة التركية. المُعاملات والإجراءات وبنية الاستجابة متطابقة، ومفتاحك يعمل على العنوانين. أما الحقول التقنية (type وrefill_status وcurrency) فتبقى بالإنجليزية حفاظًا على التوافق مع المعيار.
الانتقال إلى v3
الانتقال اختياري. وإذا قررت الانتقال فسيبقى معظم منطق عملك كما هو، لأن أسماء المُعاملات لم تتغير. الاختلاف في طريقة النقل وفي قراءة الأخطاء.
- 1انقل المفتاح من الحقل key في جسم الطلب إلى الترويسة Authorization: Bearer.
- 2استدع مسار المورد بدلًا من action=... (مثلًا POST /orders بدلًا من add).
- 3افحص الفشل عبر رمز حالة HTTP وerror.code، بدلًا من السؤال «هل يوجد حقل error؟».
- 4قارن حالة الطلب بالقيمة الآلية، لا بنص العرض.
- 5أضف Idempotency-Key عند إنشاء الطلبات.
- 6استبدل الاستعلام المتكرر عن الحالة بـ Webhooks.
يمنح المفتاح صلاحية كاملة على حسابك. لا تشاركه مع أحد، ولا تضعه في كود يعمل على جهاز العميل (client-side)، ولا ترفعه أبدًا إلى مستودع عام.
مفاتيح v3
أنشئ ما تحتاجه من مفاتيح، وأعط كل مفتاح اسمًا، وأبطل أيًا منها على حدة. لا نخزن لدينا إلا بصمة تشفيرية (digest) للمفتاح.
أنشئ حسابًا مجانيًاالمفتاح القديم
المفتاح الوحيد الذي يستخدمه API الموزّعين الكلاسيكي (v2)، ويعمل أيضًا على v3. عند توليد مفتاح جديد تبطل القيمة القديمة فورًا.
الأمان
- احفظ المفتاح في متغير بيئة، ولا تضعه أبدًا في الكود المصدري.
- لا تضع المفتاح في كود يعمل داخل المتصفح، ومرر الاستدعاءات عبر خادمك الخاص.
- أنشئ مفتاحًا منفصلًا لكل نظام، حتى لا يؤثر إبطال أحدها في الأنظمة الأخرى.
- إذا اشتبهت في تسرب المفتاح، فانشر المفتاح الجديد في أنظمتك أولًا، ثم أبطل القديم.
عندما تتغير حالة طلب، نرسل إشعارًا موقّعًا إلى خادمك، فلا تحتاج أبدًا إلى الاستعلام المتكرر عن الحالة.
لماذا Webhooks؟
الاستعلام المتكرر بطيء ومهدر في آن واحد: السؤال عن آلاف الطلبات كل دقيقة يستنزف حد طلباتك، ومع ذلك تعرف بالتغييرات بعد دقائق من حدوثها. أما مع Webhooks فيصلك التغيير لحظة حدوثه.
الإعداد
- 1جهّز عنوان URL عامًا يعمل عبر https (نرفض عناوين الشبكات المحلية والخاصة).
- 2أضف العنوان أدناه، واحفظ سر التوقيع الذي يظهر مرة واحدة فقط.
- 3تحقق من التوقيع في نظامك، ورد برمز 2xx.
- 4استخدم زر الاختبار للتأكد من المسار كاملًا من البداية إلى النهاية.
ما الذي نرسله
POST /hooks/pf HTTP/1.1
Content-Type: application/json
Webhook-Id: evt_7f910fba7cd042ef9d9069ba5c074fa0
Webhook-Timestamp: 1787261223
Webhook-Signature: t=1787261223,v1=9c1e2b0d5c6a7e91...
{
"object": "event",
"id": "evt_7f910fba7cd042ef9d9069ba5c074fa0",
"type": "order.completed",
"created_at": "2026-08-21T00:27:03.531Z",
"data": {
"previous_status": "in_progress",
"order": {
"object": "order",
"id": 23501,
"status": "completed",
"status_label": "Tamamlandı",
"service": 1234,
"quantity": 1000,
"start_count": 4210,
"remains": 0,
"charge": "1.2340",
"currency": "USD"
}
}
}التحقق من التوقيع
يحمل كل طلب ترويسة Webhook-Signature، فيها t وهو الطابع الزمني، وv1 وهو التوقيع. التوقيع هو HMAC-SHA256 للسلسلة "<timestamp>.<raw body>" محسوبًا باستخدام سرك.
- 1استخرج t وv1 من الترويسة.
- 2تأكد أن عمر t لا يزيد على 5 دقائق، لمنع إعادة إرسال الطلبات القديمة (replay).
- 3احسب HMAC-SHA256 للسلسلة "<t>.<raw body>" باستخدام سرك.
- 4قارنه بقيمة v1 بمقارنة ثابتة الزمن (constant time)، وارفض الطلب إذا لم يتطابقا.
مثال على التحقق
import crypto from "node:crypto";
import express from "express";
const app = express();
// ÖNEMLİ: imza HAM gövde üzerinden hesaplanır. JSON'a çevirip yeniden
// dizeye dönüştürürseniz boşluklar değişir ve imza tutmaz.
app.post("/hooks/pf", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const header = req.get("Webhook-Signature") ?? "";
const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header);
if (!m) return res.sendStatus(400);
const [, timestamp, signature] = m;
// Tekrar saldırısına karşı: 5 dakikadan eski damgayı reddet.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${raw}`, "utf8")
.digest("hex");
const ok =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(raw);
// 2xx dönmezseniz gönderim artan aralıklarla tekrar denenir.
res.sendStatus(200);
if (event.type === "order.completed") {
// ... siparişi kendi sisteminizde tamamlandı olarak işaretleyin
}
});إعادة المحاولة
نرسل المحاولة الأولى لحظة وقوع الحدث. إذا لم تكن الاستجابة 2xx أو فشل الاتصال، نعيد الإرسال بعد دقيقة واحدة، ثم 5 دقائق، ثم 30 دقيقة، ثم ساعتين، ثم 6 ساعات. وبعد 6 محاولات نعتبر التسليم فاشلًا، ويظهر في سجل التسليم.
أنواع الأحداث
اشترك في الأنواع التي تهمك، أو استقبلها كلها. كل تغيير في الحالة ينتج حدثًا واحدًا فقط، من النوع الأقرب إلى الحالة الجديدة.
| order.created | تم إنشاء طلب. |
| order.processing | بدأ المزوّد العمل على الطلب. |
| order.completed | اكتمل الطلب. |
| order.partial | سُلم جزء من الطلب، وأُعيد مبلغ الجزء المتبقي إلى الرصيد. |
| order.canceled | أُلغي الطلب أو استُرد مبلغه. |
| order.updated | تغيرت الحالة بطريقة أخرى. |
| refill.created | تم تقديم طلب تعويض. |
| refill.updated | تغيرت حالة طلب تعويض. |
| account.low_balance | انخفض رصيدك عن الحد الذي ضبطته عبر PATCH /account. يُطلق الحدث عند عبور الحد، لا مع كل طلب، ثم يُعاد تفعيله بعد أن يرتفع الرصيد فوق الحد. |
إذا لم تستطع استضافة Webhook
يمكنك قراءة الأحداث نفسها بالمؤشر من GET /api/v3/events. استخدمه أثناء التطوير المحلي، أو إذا لم يكن لديك عنوان IP ثابت، أو إذا كنت خلف جدار حماية.
// Webhook kuramıyorsanız (yerelde geliştirme, sabit IP yok) aynı bilgiyi
// imleçle çekebilirsiniz. İmleci kendi tarafınızda saklayın.
let cursor = loadCursor(); // en son işlediğiniz olayın "cursor" değeri
const url = new URL("https://panelfollows.com/api/v3/events");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", String(cursor));
const res = await fetch(url, { headers: { Authorization: "Bearer YOUR_API_KEY" } });
const { data, has_more } = await res.json();
for (const event of data) {
handle(event); // sizin işleyiciniz
cursor = event.cursor; // imleci ilerlet
}
saveCursor(cursor);الأخطاء (48)
| الرمز | الحالة | الوصف |
|---|---|---|
| missing_api_key | 401 | لم يتضمن الطلب مفتاح API. أرسله بالصيغة «Authorization: Bearer <key>». |
| invalid_api_key | 401 | مفتاح API الذي أرسلته غير صالح. |
| revoked_api_key | 401 | تم إبطال مفتاح API هذا، ولم يعد صالحًا للاستخدام. |
| account_banned | 403 | هذا الحساب محظور. |
| account_suspended | 403 | هذا الحساب معلق. |
| insufficient_scope | 403 | لا يملك مفتاح API هذا صلاحية الوصول إلى نقطة الاتصال هذه. |
| invalid_json | 400 | جسم الطلب ليس بصيغة JSON صالحة. |
| unsupported_content_type | 415 | قيمة Content-Type غير مدعومة. استخدم application/json أو application/x-www-form-urlencoded. |
| method_not_allowed | 405 | طريقة HTTP هذه غير مسموح بها في نقطة الاتصال هذه. |
| payload_too_large | 413 | جسم الطلب كبير جدًا. |
| missing_parameter | 400 | أحد المُعاملات الإلزامية مفقود. |
| invalid_parameter | 400 | أحد المُعاملات يحمل قيمة غير صالحة. |
| invalid_link | 400 | الرابط مفقود أو ليس عنوان http(s) صالحًا. |
| invalid_quantity | 400 | الكمية ليست عددًا صحيحًا موجبًا. |
| quantity_out_of_range | 400 | الكمية خارج النطاق المسموح به لهذه الخدمة. |
| invalid_comments | 400 | حقل التعليقات فارغ أو يتجاوز عدد الأسطر المسموح به. |
| invalid_username | 400 | اسم المستخدم غير صالح لهذه الخدمة. |
| invalid_subscription | 400 | مُعاملات الاشتراك غير صالحة. |
| invalid_runs | 400 | قيمة «runs» غير صالحة للتنقيط (drip-feed). |
| invalid_interval | 400 | قيمة «interval» غير صالحة للتنقيط (drip-feed). |
| dripfeed_not_supported | 400 | هذه الخدمة لا تدعم التنقيط (drip-feed). |
| missing_required_field | 400 | أحد الحقول الإلزامية لهذا النوع من الخدمات مفقود أو غير صالح. |
| service_inactive | 400 | هذه الخدمة غير متاحة للطلب حاليًا. |
| invalid_cursor | 400 | مؤشر ترقيم الصفحات غير صالح. |
| invalid_limit | 400 | قيمة المُعامل «limit» خارج النطاق المسموح به. |
| invalid_webhook_url | 400 | يجب أن يكون عنوان Webhook عنوانًا عامًا يبدأ بـ https://. |
| invalid_events | 400 | نوع واحد أو أكثر من أنواع الأحداث المطلوبة غير معروف. |
| batch_too_large | 400 | عدد العناصر في الطلب الجماعي الواحد أكبر من المسموح به. |
| cancel_not_supported | 400 | هذه الخدمة لا تدعم الإلغاء. |
| refill_not_supported | 400 | هذه الخدمة لا توفر التعويض. |
| unknown_endpoint | 404 | نقطة اتصال غير معروفة. راجع مرجع API لمعرفة المسارات المتاحة. |
| service_not_found | 404 | لا توجد خدمة بهذا المعرف. |
| order_not_found | 404 | لا يوجد طلب بهذا المعرف في حسابك. |
| refill_not_found | 404 | لا يوجد طلب تعويض بهذا المعرف في حسابك. |
| webhook_not_found | 404 | لا توجد نقطة اتصال Webhook بهذا المعرف في حسابك. |
| order_not_cancelable | 409 | لم يعد إلغاء هذا الطلب ممكنًا بسبب حالته الحالية. |
| cancel_rejected | 409 | رفض المزوّد طلب الإلغاء. |
| order_not_completed | 409 | لا يمكن طلب التعويض إلا لطلب مكتمل. |
| duplicate_link | 409 | يوجد بالفعل طلب نشط لهذا الرابط. انتظر حتى يكتمل. |
| idempotency_key_reuse | 409 | استُخدم مفتاح Idempotency-Key هذا من قبل مع جسم طلب مختلف. |
| idempotency_in_progress | 409 | ما زال طلب بمفتاح Idempotency-Key هذا قيد المعالجة. أعد المحاولة بعد قليل. |
| webhook_limit_reached | 409 | وصلت إلى الحد الأقصى لعدد نقاط اتصال Webhook. |
| insufficient_balance | 402 | رصيدك غير كافٍ لهذا الطلب. |
| rate_limit_exceeded | 429 | تجاوزت حد الطلبات. راجع ترويسة Retry-After في الاستجابة. |
| provider_error | 502 | أعاد المزوّد خطأ. أعد المحاولة. |
| refill_failed | 502 | رفض المزوّد طلب التعويض. |
| service_temporarily_unavailable | 503 | هذه الخدمة غير متاحة مؤقتًا. أعد المحاولة لاحقًا. |
| internal_error | 500 | حدث خطأ غير متوقع من جانبنا. |