YASend API

الويب هوك والتوقيعات

يدفع الويب هوك الأحداث إلى خادمك لحظة حدوثها: الرسائل الواردة، وإشعارات التسليم، ونتائج الإرسال غير المتزامن، وتنبيهات الجلسات.

إضافة ويب هوك

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). لذلك أجب بسرعة واحفظ ما يصلك: لا يمكننا تسليم رسالة لك مرة ثانية لاحقاً.