الويب هوك والتوقيعات
يدفع الويب هوك الأحداث إلى خادمك لحظة حدوثها: الرسائل الواردة، وإشعارات التسليم، ونتائج الإرسال غير المتزامن، وتنبيهات الجلسات.
إضافة ويب هوك
curl -X POST https://api.yasend.dev/v1/webhooks \
-H "Authorization: Bearer $YASEND_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/yasend/webhook", "secret": "'$WEBHOOK_SECRET'",
"eventTypes": ["message.incoming", "message.status", "message.sent", "message.failed", "session.alert"]}'
- النطاق: أضف
sessionIdلتستقبل أحداث جلسة واحدة فقط. بدونه تصلك أحداث كل الجلسات. - السر: عيّن دائماً
secret، وهو نص عشوائي طويل، لتتحقق أن الطلبات قادمة من YASend. - الاختبار: يرسل
POST /v1/webhooks/{id}/testحدثاً من نوعwebhook.test.
الأحداث
كل حدث هو طلب POST بمحتوى JSON يحمل event وsessionId.
event |
متى | حقول إضافية |
|---|---|---|
message.incoming |
راسل أحدهم الرقم | id (معرّف واتساب)، remoteJid، fromMe، text، timestamp |
message.status |
إشعار تسليم لرسالة مرسلة | id (معرّف واتساب)، status (sent، delivered، read، played، error)، statusCode |
message.sent |
خرجت رسالة غير متزامنة | message: كائن الرسالة مع waMessageId |
message.failed |
فشلت رسالة غير متزامنة نهائياً | message: مع error.code (مثل EXPIRED، INVALID_JID، DELIVERY_UNKNOWN) |
session.alert |
الجلسة تحتاج انتباهاً (انقطاع، تحذيرات صحة…) | level، kind، message، timestamp |
{
"event": "message.incoming",
"sessionId": "8d4c1f0e-3b9a-4f57-9a3e-2c1d0b7a6e55",
"id": "3EB0C431D2B6A1E4F9A2",
"remoteJid": "[email protected]",
"fromMe": false,
"text": "Hi, is my order ready?",
"timestamp": 1791475200
}
الترويسات
| الترويسة | |
|---|---|
X-YASend-Event |
نوع الحدث |
X-YASend-Delivery |
معرّف التسليم. هو نفسه في كل إعادة محاولة، فاستخدمه لإزالة التكرار |
X-YASend-Timestamp |
وقت Unix الذي وُقّعت فيه هذه المحاولة |
X-YASend-Signature-V2 |
t=<الوقت>,v1=<HMAC-SHA256 بالترميز السداسي لـ "<الوقت>.<المحتوى الخام>"> |
X-YASend-Signature |
sha256=<HMAC-SHA256 للمحتوى الخام> (متوافق مع النسخة المستضافة ذاتياً، والأفضل V2) |
التحقق من التوقيع
احسب الـ HMAC على محتوى الطلب الخام قبل أي تحليل لـ JSON، وقارن في زمن ثابت، وارفض الأوقات الأقدم من 5 دقائق تقريباً لمنع إعادة الإرسال.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/yasend/webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-YASend-Signature-V2") || "";
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(`${t}.`).update(req.body).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!fresh || !v1 || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
// ... handle event (quickly), then:
res.sendStatus(204);
});
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/yasend/webhook")
def webhook():
parts = dict(p.split("=", 1) for p in request.headers.get("X-YASend-Signature-V2", "").split(",") if "=" in p)
expected = hmac.new(os.environ["WEBHOOK_SECRET"].encode(),
f"{parts.get('t')}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
abort(401)
event = request.get_json()
# ... handle event
return "", 204
// routes/api.php (Laravel)
Route::post('/yasend/webhook', function (Illuminate\Http\Request $request) {
parse_str(str_replace(',', '&', $request->header('X-YASend-Signature-V2', '')), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $request->getContent(), env('WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
abort(401);
}
$event = $request->json()->all();
// ... handle event
return response()->noContent();
});
app.MapPost("/yasend/webhook", async (HttpRequest req) =>
{
using var reader = new StreamReader(req.Body);
var body = await reader.ReadToEndAsync();
var parts = req.Headers["X-YASend-Signature-V2"].ToString().Split(',')
.Select(p => p.Split('=', 2)).Where(p => p.Length == 2).ToDictionary(p => p[0], p => p[1]);
var key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("WEBHOOK_SECRET")!);
var expected = Convert.ToHexString(HMACSHA256.HashData(key,
Encoding.UTF8.GetBytes($"{parts.GetValueOrDefault("t")}.{body}"))).ToLowerInvariant();
var fresh = long.TryParse(parts.GetValueOrDefault("t"), out var t)
&& Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) < 300;
if (!fresh || !CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(parts.GetValueOrDefault("v1") ?? "")))
return Results.Unauthorized();
var evt = JsonDocument.Parse(body);
// ... handle event
return Results.NoContent();
});
أجب بسرعة؛ وإعادة المحاولات
| القاعدة | التفاصيل |
|---|---|
| أجب بسرعة | ردّ بأي حالة 2xx خلال 10 ثوانٍ، وأنجز العمل البطيء بعد الرد (في طابور مثلاً) |
| ما يُعد فشلاً | أي شيء آخر: حالة خطأ، أو انتهاء المهلة، أو خطأ اتصال |
| جدول إعادة المحاولة | بعد 30 ثانية ودقيقتين و10 دقائق وساعة و6 ساعات من الفشل (6 محاولات إجمالاً)، ثم يُعلَّم التسليم failed |
| التكرار | التسليم مضمون مرة واحدة على الأقل، فأزِل التكرار بـ X-YASend-Delivery. في أحداث الرسائل يصلح id الرسالة أيضاً |
| الترتيب | غير مضمون. استخدم timestamp وتسلسل الحالات |
| العناوين المسموحة | عناوين http(s) العامة فقط. تُرفض العناوين الخاصة وعناوين loopback وبيانات السحابة الوصفية، ولا تُتبع إعادة التوجيه |
سجل التسليم وإعادة الإرسال
| الطلب | |
|---|---|
GET /v1/webhooks/{id}/deliveries?status=failed |
كل تسليمات آخر 30 يوماً: المحاولات، وآخر حالة أعادها خادمك، وموعد إعادة المحاولة التالية |
GET /v1/webhooks/{id}/deliveries/{deliveryId} |
تسليم واحد مع محتواه كما أُرسل |
POST /v1/webhooks/{id}/deliveries/{deliveryId}/resend |
يرسل المحتوى نفسه الآن كتسليم جديد، بعد أن تصلح نقطة النهاية لديك. فقط للتسليمات التي ما زالت معلّقة أو التي ليست أحداث رسائل: انظر أدناه |
لا نحتفظ بمحتوى الرسائل. يحمل تسليم حدث رسالة (message.*) الرسالة فقط ما دامت إعادة محاولة قد تحتاجها. وبمجرد تسليمه، أو استنفاد
محاولاته، يُستبدل محتواه بـ {"event": …, "redacted": true} ولا يمكن إعادة إرساله (409 PAYLOAD_NOT_KEPT). لذلك أجب بسرعة
واحفظ ما يصلك: لا يمكننا تسليم رسالة لك مرة ثانية لاحقاً.