API v2 (تجريبي)
Webhooks
نقطة استقبال webhook هي عنوان URL على خادمك يستدعيه WbizTool عندما يحدث شيء لرسالة: أُرسلت، أو فشلت، أو أُلغيت، أو انتهت صلاحيتها. تصلك الأخبار فورًا بدلًا من الاستطلاع المتكرر، وكل طلب موقّع لتتمكن من التحقق من أنه جاء من WbizTool.
| العملية | نقطة النهاية | النطاق |
|---|---|---|
| إضافة نقطة استقبال | POST /api/v2/webhooks | messages:read |
| عرض نقاط الاستقبال | GET /api/v2/webhooks | messages:read |
| جلب نقطة استقبال | GET /api/v2/webhooks/{id} | messages:read |
| إزالة نقطة استقبال | DELETE /api/v2/webhooks/{id} | messages:read |
| استبدال سرّ التوقيع | POST /api/v2/webhooks/{id}/rotate-secret | messages:read |
| إرسال حدث تجريبي | POST /api/v2/webhooks/{id}/test | messages:read |
تخص نقاط الاستقبال مساحة العمل، وتتلقى أحداث كل رسائلها أيًّا كان مُرسِلها: API، أو لوحة التحكم، أو الحملات، أو التذكيرات، أو التكاملات. لهذا لا يستطيع مفتاح مقيّد ببعض الأرقام إدارتها: كل استدعاء يُجاب بـ 403 number_not_allowed.
الأحداث#
| النوع | متى |
|---|---|
message.sent | خرجت رسالة من رقمك. |
message.failed | تعذّر إرسال رسالة؛ ويذكر error.message السبب. |
message.cancelled | أُلغيت رسالة منتظرة: عبر API، أو من لوحة التحكم، أو لأن رقمها أو حملتها أُزيل. |
message.expired | لم تُرسَل رسالة قبل expires_at الخاص بها. |
ping | حدث تجريبي طلبته. |
تأتي لاحقًا أحداث الأرقام (الاتصال، وتسجيل الخروج) وأحداث الرسائل الواردة. لا ترسل علامتا التسليم والقراءة أحداثًا؛ اجلب الرسالة لرؤيتهما.
إضافة نقطة استقبال#
https://wbiztool.com/api/v2/webhooksالنص (Body): JSON (application/json)
الجسم
urlstringمطلوبنقطة الاستقبال الخاصة بك: عنوان
https://عام، حتى 500 حرف. تُرفض عناوينhttp://والعناوين الخاصة والمحلية (مثلlocalhostأو192.168.x.x).eventsarray of stringsاختياريأنواع الأحداث التي تتلقاها: أيٌّ من
message.sentوmessage.failedوmessage.cancelledوmessage.expired. القيمة الافتراضية: الأربعة كلها. ويصلpingدائمًا عندما تطلبه.descriptionstringاختياريملاحظة لنفسك، حتى 100 حرف، مثل
Order system.
يمكن أن يكون لمساحة العمل حتى 5 نقاط استقبال. الاستجابة هي 201 مع نقطة الاستقبال و**secret الخاص بالتوقيع، ويُعرض هذه المرة فقط**. احفظه حيث يستطيع مستقبِلك قراءته، مثل متغير بيئة. لا تعرض الاستجابات اللاحقة إلا secret_last4؛ وإذا فقدت السرّ، فاستبدله.
curl -X POST https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/wbiztool/events",
"events": ["message.sent", "message.failed"],
"description": "Order system"
}'import os
import requests
response = requests.post(
"https://wbiztool.com/api/v2/webhooks",
headers={"Authorization": f"Bearer {os.environ['WBIZTOOL_API_KEY']}"},
json={
"url": "https://example.com/wbiztool/events",
"events": ["message.sent", "message.failed"],
"description": "Order system",
},
timeout=30,
)
endpoint = response.json()
print("Store this secret now:", endpoint["secret"])// Node.js 18+ (built-in fetch). Save as .mjs to use top-level await.
const response = await fetch("https://wbiztool.com/api/v2/webhooks", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.WBIZTOOL_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.com/wbiztool/events",
events: ["message.sent", "message.failed"],
description: "Order system",
}),
});
const endpoint = await response.json();
console.log("Store this secret now:", endpoint.secret);<?php
$ch = curl_init('https://wbiztool.com/api/v2/webhooks');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WBIZTOOL_API_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/wbiztool/events',
'events' => ['message.sent', 'message.failed'],
'description' => 'Order system',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$endpoint = json_decode(curl_exec($ch), true);
curl_close($ch);
echo 'Store this secret now: ' . $endpoint['secret'];الاستجابة (201):
{
"id": 41,
"url": "https://example.com/wbiztool/events",
"description": "Order system",
"events": ["message.sent", "message.failed"],
"secret_last4": "9c1e",
"active": true,
"disabled_reason": null,
"created_at": "2026-10-18T11:20:05.733Z",
"last_delivery": null,
"secret": "3f0b…9c1e"
}
قيمة secret الحقيقية 64 حرفًا ست عشريًا. إعادة محاولة هذا الطلب مع Idempotency-Key نفسه لا تضيف نقطة استقبال ثانية، ولا تتضمن استجابتها السرّ.
أخطاء إضافة نقطة استقبال#
| HTTP | الرمز | متى |
|---|---|---|
400 | validation_failed | url مفقود أو طويل جدًا، أو events فارغ أو يحتوي على نوع غير معروف، أو أن حقلًا غير معروف |
403 | webhook_limit_reached | لدى مساحة العمل بالفعل 5 نقاط استقبال |
403 | number_not_allowed | المفتاح مقيّد ببعض الأرقام |
422 | invalid_webhook_url | url ليس عنوان https:// عامًا |
عرض نقاط الاستقبال#
https://wbiztool.com/api/v2/webhooksنقاط استقبال مساحة العمل التي تتلقى الأحداث، من الأقدم إلى الأحدث، في قائمة data واحدة.
curl https://wbiztool.com/api/v2/webhooks \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
لا تتضمن القائمة نقاط الاستقبال التي توقفت. ويرى المالكون نقاط الاستقبال أيضًا، ويمكنهم إزالتها، في الإعدادات ← مفاتيح API تحت نقاط استقبال webhook.
جلب نقطة استقبال#
https://wbiztool.com/api/v2/webhooks/{id}نقطة استقبال واحدة. يُظهر last_delivery كيف جرى آخر تسليم لها. نقطة الاستقبال غير الموجودة أو التي أُزيلت تُجاب بـ 404 webhook_not_found؛ أما النقطة المتوقفة فتُعرض مع active: false.
إزالة نقطة استقبال#
https://wbiztool.com/api/v2/webhooks/{id}لا يُرسَل إليها شيء بعد ذلك، بما في ذلك إعادات المحاولة التي لا تزال منتظرة. الاستجابة هي {"id": 41, "deleted": true}.
استبدال سرّ التوقيع#
https://wbiztool.com/api/v2/webhooks/{id}/rotate-secretيُنشئ سرًّا جديدًا لنقطة الاستقبال. الاستجابة هي 200 مع نقطة الاستقبال وsecret الجديد الخاص بها، ويُعرض هذه المرة فقط. يتوقف السرّ القديم فورًا: الأحداث المرسلة بعد ذلك تُوقَّع بالسرّ الجديد، بما فيها إعادات محاولة الأحداث السابقة. حدّث مستقبِلك فورًا.
curl -X POST https://wbiztool.com/api/v2/webhooks/41/rotate-secret \
-H "Authorization: Bearer $WBIZTOOL_API_KEY"
إرسال حدث تجريبي#
https://wbiztool.com/api/v2/webhooks/{id}/testيضع حدث ping في قائمة الانتظار لنقطة الاستقبال هذه، موقّعًا مثل كل حدث، لتتمكن من اختبار مستقبِلك. الاستجابة هي 202:
{ "event_id": "evt_8a2f4c1d9b7e6a5f4c3b2a19", "type": "ping", "queued": true }
تتلقى نقطة الاستقبال لديك:
{
"id": "evt_8a2f4c1d9b7e6a5f4c3b2a19",
"type": "ping",
"created_at": "2026-10-18T11:21:40.002Z",
"data": { "webhook_id": 41 }
}
حمولة الحدث#
كل حدث هو طلب POST بجسم JSON:
{
"id": "evt_c41d7e2a9f3b8c6d5e4f3a21",
"type": "message.sent",
"created_at": "2026-10-18T10:15:09.912Z",
"data": {
"message": {
"id": 9817263,
"status": "sent",
"from": { "id": 101, "phone": "919800000000", "platform": "whatsapp" },
"to": { "phone": "+919876543210", "contact_id": 991 },
"text": "Hi Asha, your order #1042 is ready.",
"media": null,
"send_at": null,
"expires_at": null,
"created_at": "2026-10-18T10:15:02.311Z",
"sent_at": "2026-10-18T10:15:09.870Z",
"error": null
}
}
}
| الحقل | الوصف |
|---|---|
id | معرّف الحدث، evt_ متبوعًا بـ 24 حرفًا. يحتفظ الحدث نفسه بمعرّفه في كل إعادة محاولة. |
type | أحد أنواع الأحداث. |
created_at | وقت وقوع الحدث. |
data.message | لأحداث message.*: الرسالة كما كانت حينها. يعمل media.url فيها لمدة 26 ساعة على الأقل. |
data.webhook_id | لحدث ping: معرّف نقطة الاستقبال. |
الترويسات:
| الترويسة | القيمة |
|---|---|
Content-Type | application/json |
User-Agent | WbizTool-Webhooks/2 |
WbizTool-Event-Id | id الخاص بالحدث، وهو نفسه في كل إعادة محاولة |
WbizTool-Timestamp | وقت إرسال هذه المحاولة، بثواني Unix، مثل 1792310409 |
WbizTool-Signature | v1= متبوعًا بقيمة HMAC-SHA256 الست عشرية لـ <timestamp>.<raw body>، محسوبةً بسرّ نقطة الاستقبال |
التحقق من التوقيع#
تحقّق من كل طلب قبل أن تثق به: احسب التوقيع من الجسم الخام (قبل أن يحلّل إطار العمل لديك JSON)، وقارنه بالترويسة بمقارنة ثابتة الزمن، وارفض الطوابع الزمنية التي تبعد أكثر من 5 دقائق، حتى لا يمكن إعادة تشغيل طلب قديم. إنه المخطط نفسه المستخدم في توقيعات webhook في API v1، مع سرّ نقطة الاستقبال الخاص بها.
ثم استخدم WbizTool-Event-Id لتجاهل التكرارات: قد يصل الحدث نفسه أكثر من مرة، مثلًا عندما تضيع استجابتك فنعيد المحاولة.
import hashlib, hmac, json, os, time
SECRET = os.environ["WBIZTOOL_WEBHOOK_SECRET"]
def verify(secret: str, body: bytes, headers) -> bool:
ts, sig = headers.get("WbizTool-Timestamp", ""), headers.get("WbizTool-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
mac = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig)
# Flask
from flask import Flask, request
app = Flask(__name__)
seen = set() # use your database in production
@app.post("/wbiztool/events")
def events():
body = request.get_data() # the raw bytes
if not verify(SECRET, body, request.headers):
return "", 401
event_id = request.headers["WbizTool-Event-Id"]
if event_id in seen:
return "", 200 # a repeat: already handled
seen.add(event_id)
event = json.loads(body)
if event["type"] == "message.failed":
message = event["data"]["message"]
print("Failed:", message["id"], message["error"]["message"])
return "", 200في Django، مرّر request.body وrequest.headers.
const crypto = require("crypto");
const express = require("express");
const SECRET = process.env.WBIZTOOL_WEBHOOK_SECRET;
function verify(secret, rawBody, headers) {
const ts = String(headers["wbiztool-timestamp"] || ""), sig = String(headers["wbiztool-signature"] || "");
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = "v1=" + crypto.createHmac("sha256", secret).update(ts + ".").update(rawBody).digest("hex");
return sig.length === mac.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mac));
}
const app = express();
const seen = new Set(); // use your database in production
// express.raw keeps the body as a Buffer: the signature covers the exact bytes.
app.post("/wbiztool/events", express.raw({ type: "*/*" }), (req, res) => {
if (!verify(SECRET, req.body, req.headers)) return res.sendStatus(401);
const eventId = req.headers["wbiztool-event-id"];
if (seen.has(eventId)) return res.sendStatus(200); // a repeat: already handled
seen.add(eventId);
const event = JSON.parse(req.body);
if (event.type === "message.failed") {
const message = event.data.message;
console.log("Failed:", message.id, message.error.message);
}
res.sendStatus(200);
});
app.listen(3000);أجب بـ 2xx بسرعة، ثم أنجز العمل البطيء بعد ذلك (في قائمة انتظار مثلًا).
إعادة المحاولة#
- أجب بأي
2xxخلال 10 ثوانٍ. وأي شيء آخر يُعدّ فشلًا: رمز حالة آخر (بما في ذلك4xx)، أو عدم الإجابة خلال 10 ثوانٍ، أو عدم الاتصال خلال 3 ثوانٍ. تُتبَع عمليات إعادة التوجيه، 3 على الأكثر، ويجب أن تؤدي كل منها إلى عنوان عام. - يُعاد إرسال التسليم الفاشل بعد 1 و5 و15 دقيقة، ثم بعد 1 و2 و4 و8 و8 ساعات: 9 محاولات على مدى 23 ساعة تقريبًا. وبعد آخرها، يُتجاهل ذلك الحدث.
- تحمل إعادة المحاولة الجسم نفسه و
WbizTool-Event-Idنفسه، معWbizTool-Timestampوتوقيع جديدين. - الترتيب غير مضمون. قد يصل
message.sentالمُعاد إرساله بعد حدث لاحق. استخدمstatusالخاص بالرسالة، أو اجلب الرسالة، عندما يهمّك الترتيب.
يُظهر last_delivery في نقطة الاستقبال كيف جرى آخر تسليم: state (pending أو delivered أو failed)، وstatus_code الذي أجاب به خادمك، ووقت إنشاء الحدث.
متى تتوقف نقاط الاستقبال#
تتوقف نقطة الاستقبال عن تلقي الأحداث عند حدوث أحد ما يلي. وتبقى في GET /webhooks مع active: false وdisabled_reason، لتعرف السبب؛ ولا يزال من الممكن قراءتها وإزالتها، لكن لا يمكن اختبارها ولا منحها سرًّا جديدًا (404).
- فشلت لمدة 24 ساعة. إذا لم يُسلَّم إليها شيء طوال 24 ساعة بينما استمرت عمليات التسليم في الفشل، فإنها تُوقَف وتُتجاهل إعادات المحاولة المنتظرة. وتظهر حينها في الإعدادات ← مفاتيح API بالحالة متوقفة. أصلح خادمك، ثم أضف نقطة الاستقبال مرة أخرى.
- أُبطل المفتاح الذي أنشأها أو انتهت صلاحيته. إبطال مفتاح يوقف كل ما أعدّه. أضف نقطة الاستقبال مرة أخرى بمفتاح آخر.
- أُزيلت، عبر إزالة نقطة استقبال أو في الإعدادات ← مفاتيح API.
نقاط الاستقبال المتوقفة لا تُحتسب ضمن الحد البالغ 5.
كائن نقطة استقبال webhook#
| الحقل | النوع | الوصف |
|---|---|---|
id | integer | معرّف نقطة الاستقبال. |
url | string | المكان الذي تُرسَل إليه الأحداث. |
description | string | ملاحظتك؛ "" عندما لا توجد ملاحظة. |
events | array | أنواع الأحداث التي تتلقاها. |
secret | string | سرّ التوقيع: يظهر فقط في استجابتي إضافة نقطة استقبال واستبدال سرّ التوقيع. |
secret_last4 | string | آخر 4 أحرف من السرّ، لتعرف أيّ سرّ لديك. |
active | boolean | false عندما لم تعد تتلقى أحداثًا – راجع متى تتوقف نقاط الاستقبال. |
disabled_reason | string or null | سبب توقفها (الفشل لمدة 24 ساعة، أو أن المفتاح الذي أنشأها أُبطل أو انتهت صلاحيته)؛ وnull ما دامت نشطة. |
created_at | string | وقت إضافتها. |
last_delivery | object or null | آخر تسليم جرت محاولته: state، وstatus_code (أو null عندما لم يُجب خادمك)، وcreated_at. وتكون null قبل التسليم الأول. |
صفحات ذات صلة#
- الرسائل: كائن الرسالة في
data.message - توقيعات webhook في API v1
- الأخطاء
