الـ Webhooks
ليصلك خبر أي تغيير في مهنارو لحظة حدوثه: الأحداث، وما تحمله كل رسالة، والتحقق من التوقيع بـ PHP وJavaScript وPython، وإعادة المحاولة، وسجل التسليم.
الـ webhook عنوان على خادمك يتصل به مهنارو كلما حدث شيء في شركتك: إضافة موظف، أو الموافقة على إجازة، أو اعتماد الرواتب. فلا تحتاج أن تسأل الـ API عن التغييرات باستمرار.
إضافة webhook
- افتح الشركة ← مفاتيح API والـ Webhooks ثم تبويب Webhooks (تحتاج صلاحية إدارة مفاتيح API والـ webhooks).
- اختر إضافة webhook واملأ:
- الاسم، والرابط الذي سيستقبل الرسائل. يجب أن يكون
httpأوhttpsويصل إليه الإنترنت: عناوين الشبكات الخاصة وlocalhostمرفوضة. - الشكل: JSON (الافتراضي) أو XML.
- الأحداث: الأحداث التي تُرسل (انظر أدناه).
- اختيارياً المصادقة بمفتاح API: عندها يرسل مهنارو مع كل رسالة المصادقة الصادرة لذلك المفتاح (Basic أو OAuth 1 أو OAuth 2 أو اسم مستخدم وكلمة مرور).
- الاسم، والرابط الذي سيستقبل الرسائل. يجب أن يكون
- احفظ. سر التوقيع (
whsec_…) يظهر مرة واحدة: انسخه إلى نظامك المستقبِل. - استخدم إرسال اختبار لإرسال حدث
testفوراً ورؤية الرد في سجل التسليم.
تستطيع أيضاً إدارة الـ webhooks بالـ API (POST /api/v1/app/companies/{company}/webhooks، ويرجع data.secret مرة واحدة) أو بأداة MCP manage_webhook، برمز دخول شخص. مفاتيح API لا تدير الـ webhooks.
الشركة التجريبية لا ترسل webhooks أبداً.
الأحداث
| المجموعة | الحدث | يُرسل عندما |
|---|---|---|
| الموظف | employee. |
يُضاف موظف (بأي طريقة: الشاشات، الـ API، MCP، الاستيراد) |
| الموظف | employee. |
يتغير سجل موظف؛ وchanged تذكر الحقول |
| الهيكل التنظيمي | org.، org.، org. |
يُضاف فرع أو يُعدَّل أو يُحذف |
| الهيكل التنظيمي | org.، org.، org. |
يُضاف قسم أو وحدة أخرى أو تُعدَّل أو تُحذف |
| الراتب | salary. |
يتغير راتب موظف |
| الراتب | payroll. |
يُعتمد مسير رواتب |
| الراتب | payroll. |
يُعتمد مسير رواتب: قيود اليومية الخاصة به، للمحاسبة |
| السلفة | loan.، loan. |
تُعطى سلفة، أو تُسدَّد |
| المعاملة المالية | financial_transaction.، financial_transaction. |
يُضاف استحقاق أو استقطاع أو يُحذف |
| إنهاء الخدمة | employee. |
تنتهي خدمة موظف |
| الطلبات | leave.، leave. |
يُبَتّ في إجازة |
| الطلبات | request.، request. |
يُبَتّ في طلب آخر |
GET /api/v1/app/companies/{company}/webhook-events يرجع نفس القائمة مع الأشكال وجدول إعادة المحاولة.
الرسالة
كل تسليم طلب POST إلى رابطك:
POST /hooks/mihnaro HTTP/1.1
Content-Type: application/json
User-Agent: Mihnaro-Webhooks/1.0
X-Mihnaro-Event: test
X-Mihnaro-Delivery: 0b6f2a52-5c1e-4c43-9a4e-1f0e2d8c7b11
X-Mihnaro-Signature: t=1793650158,v1=48a0460620a1d0e6ce9c52e941916366c0781382c3854e4979d8b485107864b7
{"id":"0b6f2a52-5c1e-4c43-9a4e-1f0e2d8c7b11","event":"test","company_id":16,"sent_at":"2026-11-02T22:40:00+03:00","data":{"message":"Mihnaro is connected to this webhook."}}
idهو نفسهX-Mihnaro-Delivery، ويبقى كما هو في كل إعادة محاولة وإعادة إرسال: استخدمه لتجاوز الرسائل التي عالجتها سابقاً.sent_atبساعة الشركة مع فرق التوقيت.- مع شكل XML تأتي نفس الرسالة بالشكل
<webhook><event/><sent_at/><data>…</data></webhook>، وعناصر القوائم<item>.
ما يحمله data لكل حدث:
{"event": "employee.created", "data": {"id": 6211, "employee_number": "EMP-0001", "name": {"en": "Ali Hassan", "ar": "علي حسن"}, "branch_id": 76, "status": "active"}}
{"event": "employee.updated", "data": {"id": 6211, "employee_number": "EMP-0001", "name": {"en": "Ali Hassan", "ar": "علي حسن"}, "branch_id": 76, "status": "active", "changed": ["phone", "department_id"]}}
{"event": "employee.terminated", "data": {"id": 6211, "employee_number": "EMP-0001", "name": {"en": "Ali Hassan", "ar": "علي حسن"}, "branch_id": 76, "status": "terminated", "terminated_on": "2026-11-30"}}
{"event": "org.branch.created", "data": {"id": 77, "name": {"en": "Basra", "ar": "البصرة"}, "code": "BSR"}}
{"event": "org.unit.updated", "data": {"id": 2905, "type": "department", "name": {"en": "Human Resources", "ar": "الموارد البشرية"}, "parent_id": null}}
{"event": "salary.changed", "data": {"employee_id": 6211, "old_salary": 1500000, "new_salary": 1750000, "effective_date": "2026-11-01"}}
{"event": "loan.created", "data": {"id": 31, "employee_id": 6211, "amount": 3000000, "installments": 10}}
{"event": "loan.paid", "data": {"id": 31, "employee_id": 6211, "amount": 3000000, "paid_at": "2027-09-01T10:15:00+03:00"}}
{"event": "financial_transaction.created", "data": {"id": 120, "employee_id": 6211, "type": "earning", "amount": 250000, "month": "2026-11"}}
{"event": "leave.approved", "data": {"id": 88, "employee_id": 6212, "leave_type_id": 41, "start_date": "2026-11-09", "end_date": "2026-11-11", "days": 3}}
{"event": "request.rejected", "data": {"id": 52, "employee_id": 6212, "type": "overtime"}}
{"event": "payroll.approved", "data": {"id": 14, "month": "2026-10", "kind": "regular", "employees": 10, "net": 18450000, "total_net": 18450000}}
{"event": "payroll.journal", "data": {"id": 14, "month": "2026-10", "kind": "regular", "lines": [{"journal": "…", "account": "…", "description": "…", "group": "…", "debit": 18450000, "credit": 0, "employee": null}]}}
أسماء الحقول دقيقة، والقيم هنا أمثلة. أحداث الإجازات والطلبات تحمل رقم الموظف فقط: استدعِ GET /api/v1/app/companies/{company}/employees/{employee} لبقية بياناته.
التحقق من التوقيع
كل رسالة موقّعة بسر الـ webhook، فتعرف أنها من مهنارو ولم تُغيَّر. الترويسة X-Mihnaro-Signature: t=<unix time>,v1=<signature> (t وقت الإرسال بثواني unix)، والتوقيع هو HMAC-SHA256 بصيغة hex للوقت ثم نقطة ثم نص الرسالة الخام كما وصل تماماً.
- اقرأ النص الخام قبل أي تحويل JSON (التحويل وإعادة الترميز يغيّرانه).
- احسب
HMAC_SHA256(secret, t + "." + body)وقارنه بـv1بمقارنة ثابتة الوقت. - ارفض الرسائل التي يبعد
tفيها أكثر من 5 دقائق عن الآن، حتى لا تُعاد رسالة قديمة. - ردّ بأي
2xxبسرعة (خلال 10 ثوانٍ)، ثم أنجز العمل.
PHP
<?php
function mihnaro_verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
if (! preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m)) {
return false;
}
[, $timestamp, $signature] = $m;
$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);
return hash_equals($expected, $signature) && abs(time() - (int) $timestamp) <= $tolerance;
}
$body = file_get_contents('php://input');
if (! mihnaro_verify($body, $_SERVER['HTTP_X_MIHNARO_SIGNATURE'] ?? '', getenv('MIHNARO_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$message = json_decode($body, true);
http_response_code(200);
JavaScript (Node، Express)
import crypto from 'node:crypto';
import express from 'express';
function verifyMihnaro(rawBody, header, secret, toleranceSeconds = 300) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? '');
if (!match) return false;
const [, timestamp, signature] = match;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= toleranceSeconds;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
const app = express();
app.post('/hooks/mihnaro', express.raw({ type: '*/*' }), (req, res) => {
const body = req.body.toString('utf8');
if (!verifyMihnaro(body, req.get('X-Mihnaro-Signature'), process.env.MIHNARO_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const message = JSON.parse(body);
res.sendStatus(200);
});
app.listen(3000);
Python (Flask)
import hashlib, hmac, os, re, time
from flask import Flask, request, abort
def verify_mihnaro(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
match = re.fullmatch(r"t=(\d+),v1=([a-f0-9]{64})", header or "")
if not match:
return False
timestamp, signature = match.groups()
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature) and abs(time.time() - int(timestamp)) <= tolerance
app = Flask(__name__)
@app.post("/hooks/mihnaro")
def mihnaro_hook():
if not verify_mihnaro(request.get_data(), request.headers.get("X-Mihnaro-Signature", ""), os.environ["MIHNARO_WEBHOOK_SECRET"]):
abort(400)
message = request.get_json()
return "", 200
جرّبنا دوال التحقق الثلاث على توقيع حقيقي من مهنارو: كل منها تقبله، وكل منها ترفض نفس الرسالة بحرف زائد واحد.
إعادة المحاولة وسجل التسليم
- ينتظر مهنارو ردك حتى 10 ثوانٍ ولا يتبع التحويلات. أي
2xxيعني تم التسليم. - غير ذلك يُعاد بعد 1 و5 و30 و120 و720 دقيقة (نحو 14 ساعة ونصف إجمالاً). بعد آخر محاولة يبقى التسليم لم يُسلَّم، وتستطيع إعادة إرساله يدوياً.
- كل محاولة محفوظة في تبويب سجل التسليم: الوقت، والرمز الذي رد به خادمك، وبداية رده. افتح أي تسليم لترى الرسالة ومحاولاتها، واختر إعادة الإرسال.
- بالـ API:
GET /api/v1/app/companies/{company}/webhook-deliveries(webhook_idوstatus)، وGET …/webhook-deliveries/{delivery}، وPOST …/webhook-deliveries/{delivery}/resend. - سر توقيع جديد على أي webhook ينشئ سراً جديداً (يظهر مرة واحدة)، والقديم يتوقف فوراً.
آخر تحديث: