الـ webhooks
إدارة الـ webhooks
يسجّل عنوان https عامًا يستقبل حدثًا موقّعًا واحدًا لكل استدعاء عندما يتغير طلب أو إعادة تعبئة. تتبع نقاط النهاية الحساب، ولكل منها سرّه الخاص.
https://notpanel.com/api/v3action=webhook.add- مفتاح API مطلوب
- تجاوز حد الطلبات
- الجسم:
application/x-www-form-urlencoded
المعاملات
يُرسل دائمًا
keystringمطلوب- مفتاح API الخاص بك.
actionstringمطلوبwebhook.addيجب أن يكون السلسلة النصية الحرفية "webhook.add".
معاملات الإجراء
urlstring (URL)مطلوب- عنوان https عام يستقبل الأحداث.
eventsstring (CSV)اختياري- أنواع الأحداث المراد الاشتراك فيها، مفصولة بفواصل، من أصل 8. اتركه فارغًا لاستقبالها كلها.order.processing, order.in_progress, order.completed, order.partial, order.canceled, order.cancel_refused, refill.completed, refill.rejected
curl -X POST https://notpanel.com/api/v3 \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "key=YOUR_API_KEY&action=webhook.add&url=https://your-server.example.com/notpanel-webhook&events=order.completed,order.partial,order.canceled"const res = await fetch("https://notpanel.com/api/v3", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
key: "YOUR_API_KEY",
action: "webhook.add",
url: "https://your-server.example.com/notpanel-webhook",
events: "order.completed,order.partial,order.canceled",
}),
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
"https://notpanel.com/api/v3",
data={
"key": "YOUR_API_KEY",
"action": "webhook.add",
"url": "https://your-server.example.com/notpanel-webhook",
"events": "order.completed,order.partial,order.canceled",
},
)
print(res.json())<?php
$body = http_build_query([
'key' => 'YOUR_API_KEY',
'action' => 'webhook.add',
'url' => 'https://your-server.example.com/notpanel-webhook',
'events' => 'order.completed,order.partial,order.canceled',
]);
$response = file_get_contents('https://notpanel.com/api/v3', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => $body,
'ignore_errors' => true,
],
]));
print_r(json_decode($response, true));
استبدل YOUR_API_KEY بالمفتاح من صفحة API.
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"url": "https://your-server.example.com/notpanel-webhook",
"events": ["order.completed", "order.partial", "order.canceled"],
"active": true,
"paused_at": null,
"created_at": "2026-10-03T09:00:00Z",
"status": "active",
"failureCount": 0,
"lastDeliveredAt": null,
"createdAt": "2026-10-03T09:00:00.000Z",
"secret": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"webhook_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
}الأخطاء
- 400
{"error":"Invalid webhook URL","error_code":"INVALID_WEBHOOK_URL","retryable":false}العنوان ليس عنوان https عامًا، أو يشير إلى عنوان خاص.
- 400
{"error":"Invalid events: not_allowed","error_code":"INVALID_PARAMETER","retryable":false,"details":{"param":"events","rule":"not_allowed"}}اسم حدث غير موجود في القائمة. تحقق من الإملاء: الأسماء تشبه
order.completed. - 409
{"error":"Maximum 5 webhook endpoints per account","error_code":"WEBHOOK_LIMIT","retryable":false,"details":{"limit":5}}لدى الحساب بالفعل الحد الأقصى وهو 5 من نقاط النهاية. أزل واحدة أولًا.
الأحداث
اشترك في أي منها. يُرسل كل حدث كاستدعاء مستقل عند حفظ التغيير، والتغيير الذي يُتراجع عنه لا يرسل شيئًا. لا يُرسل ping إلا عبر webhook.test.
أنواع الأحداث
order.processing- أُرسل الطلب وهو ينتظر بدء التسليم.
order.in_progress- بدأ التسليم.
order.completed- سُلِّم الطلب بالكامل.
order.partial- سُلِّم جزء من الطلب وأُعيد الباقي.
order.canceled- أُلغي الطلب وأُعيد المبلغ. يحل محل
order.refundedالسابق. order.cancel_refused- رُفض طلب إلغاء. يستمر الطلب في العمل.
refill.completed- تمت إعادة تعبئة. يحل محل
order.refill_completedالسابق. refill.rejected- رُفضت إعادة تعبئة. تحمل البيانات السبب.
ping- حدث اختبار يُرسل عبر
webhook.testفقط.
عرض نقاط النهاية
يعيد action=webhook.list كل نقطة نهاية للحساب مع أحداثها، وهل هي نشطة، ومتى أُوقفت مؤقتًا إن أُوقفت. لا يُعاد السرّ أبدًا. وتُضمَّن أيضًا المفاتيح السابقة status وfailureCount وlastDeliveredAt وcreatedAt.
https://notpanel.com/api/v3action=webhook.listkey=YOUR_API_KEY&action=webhook.list[
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"url": "https://your-server.example.com/notpanel-webhook",
"events": ["order.completed", "order.partial", "order.canceled"],
"active": true,
"paused_at": null,
"created_at": "2026-10-03T09:00:00Z",
"status": "active",
"failureCount": 0,
"lastDeliveredAt": null,
"createdAt": "2026-10-03T09:00:00.000Z"
}
]إرسال حدث تجريبي
يضع action=webhook.test حدث ping في قائمة الانتظار إلى نقطة النهاية. استخدمه للتحقق من مستقبلك ومن شيفرة التوقيع. يحمل الردّ معرّف التسليم.
https://notpanel.com/api/v3action=webhook.testkey=YOUR_API_KEY&action=webhook.test&id=3f2504e0-4f89-41d3-9a0c-0305e82c3301{
"queued": true,
"delivery_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}إزالة نقطة نهاية
يحذف action=webhook.remove نقطة النهاية وسرّها. قد يصل تسليم كان في الطريق بالفعل؛ ولا يوضع أي شيء جديد في قائمة الانتظار لها.
https://notpanel.com/api/v3action=webhook.removekey=YOUR_API_KEY&action=webhook.remove&id=3f2504e0-4f89-41d3-9a0c-0305e82c3301{
"removed": true,
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"success": true
}حمولة التسليم
كل تسليم هو حدث واحد في POST واحد مع Content-Type: application/json. الجسم غلاف، ويحمل data الطلب أو إعادة التعبئة بالصيغة نفسها المستخدمة في status وrefill_status. هذه هي نسخة المخطط 1.
الغلاف
idstring- معرّف فريد للحدث. استخدمه لتجاهل التكرارات.
typestring- نوع الحدث.
schema_versioninteger- نسخة هذه الصيغة. هي 1 حاليًا.
created_atstring- متى أُنشئ الحدث.
sequenceinteger- يزداد مع كل حدث في الحساب، لتتمكن من ترتيب الأحداث.
dataobjectorderأوrefill، بصيغة إجراء القراءة المقابل. فارغ في حالة ping.
ترويسات الطلب
X-Webhook-Signature- HMAC-SHA256 للطابع الزمني ونقطة والجسم بالضبط، مكتوبًا بصيغة
sha256=يليها الأرقام الست عشرية. X-Webhook-Timestamp- وقت Unix بالثواني لهذه المحاولة. تحصل إعادة المحاولة على طابع زمني وتوقيع جديدين.
X-Webhook-Event- نوع الحدث، وهو نفسه
typeفي الجسم. X-Webhook-Delivery-Id- معرّف هذا التسليم، وهو نفسه في كل إعادة محاولة له.
{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e51",
"type": "order.completed",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1042,
"data": {
"order": {
"charge": "1.20",
"start_count": "3572",
"status": "Completed",
"remains": "0",
"currency": "USD",
"order": 1234567,
"status_key": "completed",
"refund": "0.00",
"reason_code": null,
"reason": null,
"can_cancel": false,
"cancel_available": false,
"cancel_state": null,
"can_refill": false,
"refill_available_at": "2026-10-03T18:35:00Z",
"updated_at": "2026-10-03T12:35:00Z"
}
}
}{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e52",
"type": "order.partial",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1043,
"data": {
"order": {
"charge": "0.87",
"start_count": "3572",
"status": "Partial",
"remains": "157",
"currency": "USD",
"order": 1234568,
"status_key": "partial",
"refund": "0.13",
"reason_code": "partially_delivered",
"reason": "Part of the order was delivered. The rest was refunded.",
"can_cancel": false,
"cancel_available": false,
"cancel_state": null,
"can_refill": true,
"refill_available_at": null,
"updated_at": "2026-10-03T12:00:00Z"
}
}
}{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e53",
"type": "order.canceled",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1044,
"data": {
"order": {
"charge": "0.00",
"start_count": "0",
"status": "Canceled",
"remains": "1000",
"currency": "USD",
"order": 1234569,
"status_key": "cancelled",
"refund": "1.20",
"reason_code": "customer_canceled",
"reason": "You canceled this order. The charge was refunded.",
"can_cancel": false,
"cancel_available": false,
"cancel_state": null,
"can_refill": false,
"refill_available_at": null,
"updated_at": "2026-10-03T10:00:09Z"
}
}
}{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e56",
"type": "refill.rejected",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1047,
"data": {
"refill": {
"refill": 5502,
"status": "Rejected",
"status_key": "rejected",
"order": 1234568,
"reason_code": "refill_too_early",
"reason": "This order cannot be refilled yet. Try again later.",
"created_at": "2026-10-03T12:40:00Z",
"updated_at": "2026-10-03T12:41:00Z"
}
}
}التحقق من التوقيعات
اقرأ X-Webhook-Timestamp والبايتات الدقيقة للجسم. احسب HMAC-SHA256 على <timestamp>.<body bytes> بسرّ نقطة النهاية، واكتبه بصيغة sha256=<hex>، وقارنه بـ X-Webhook-Signature في زمن ثابت. لإعادة المحاولة معرّف الحدث نفسه لكن بطابع زمني وتوقيع جديدين.
اقرأ الجسم الخام والترويسات
احسب التوقيع وقارنه
تخطَّ الأحداث التي عالجتها بالفعل
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* @param {{rawBody: unknown, signatureHeader: unknown, timestampHeader: unknown,
* secret: unknown, nowSeconds?: number, toleranceSeconds?: number}} input
*/
function verifyNotPanelBody({
rawBody,
signatureHeader,
timestampHeader,
secret,
nowSeconds = Math.floor(Date.now() / 1000),
toleranceSeconds = 300, // Example receiver policy, not a NotPanel rule.
}) {
if (!Buffer.isBuffer(rawBody)) return false;
if (typeof secret !== "string" || secret.length === 0) return false;
if (typeof signatureHeader !== "string") return false;
if (typeof timestampHeader !== "string") return false;
if (!/^(0|[1-9]\d{0,15})$/.test(timestampHeader)) return false;
if (!Number.isSafeInteger(nowSeconds)) return false;
if (!Number.isSafeInteger(toleranceSeconds) || toleranceSeconds < 0) {
return false;
}
const timestamp = Number(timestampHeader);
if (!Number.isSafeInteger(timestamp)) return false;
if (Math.abs(nowSeconds - timestamp) > toleranceSeconds) return false;
const match = /^sha256=([0-9a-f]{64})$/i.exec(signatureHeader);
if (!match?.[1]) return false;
const expected = createHmac("sha256", secret)
.update(timestampHeader, "utf8")
.update(".", "utf8")
.update(rawBody)
.digest();
const received = Buffer.from(match[1], "hex");
return timingSafeEqual(expected, received);
}مثال محلول
استخدمه لاختبار شيفرتك. يغطي التوقيع الطابع الزمني ونقطة والجسم تمامًا كما هو معروض، دون مسافات أو فواصل أسطر إضافية.
secret = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
timestamp = 1790000000
body = {"id":"e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e51","type":"order.completed","schema_version":1,"created_at":"2026-10-03T12:30:00Z","sequence":1042,"data":{"order":{"charge":"1.20","start_count":"3572","status":"Completed","remains":"0","currency":"USD","order":1234567,"status_key":"completed","refund":"0.00","reason_code":null,"reason":null,"can_cancel":false,"cancel_available":false,"cancel_state":null,"can_refill":false,"refill_available_at":"2026-10-03T18:35:00Z","updated_at":"2026-10-03T12:35:00Z"}}}X-Webhook-Signature: sha256=6e8e90578f566d2e14b5c3914eba7e378c4107a5a4d4ddd52c747791f2264fffالتسليم وإعادة المحاولة
مرة واحدة على الأقل
أي ردّ 2xx خلال 10 ثانية يُعد تسليمًا. لا تُتّبع إعادة التوجيه، ويُقرأ أول 4096 بايت فقط من ردّك. قد يصل الحدث نفسه أكثر من مرة، لذا تجاهل معرّف حدث عالجته بالفعل.
ثماني محاولات على مدى يوم
8 محاولات في المجموع: الأولى فورًا، ثم 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h بعد الحدث. تحتفظ كل محاولة بمعرّف الحدث ومعرّف التسليم نفسيهما.
تتوقف نقاط النهاية مؤقتًا من تلقاء نفسها
بعد 50 إخفاقات متتالية، أو 3 أيام دون نجاح، تُوقف نقطة النهاية مؤقتًا ويُبلَّغ مالك الحساب. أصلح مستقبلك وأضف نقطة النهاية مجددًا.
الترتيب غير مضمون
قد تصل الأحداث غير مرتبة. استخدم
sequenceلترتيبها، وعالج كل معرّف حدث مرة واحدة، واقرأaction=statusعندما تحتاج إلى الحالة الحالية.