انتقل إلى المحتوى
مهنارو
مركز المساعدة

الـ Webhooks

ليصلك خبر أي تغيير في مهنارو لحظة حدوثه: الأحداث، وما تحمله كل رسالة، والتحقق من التوقيع بـ PHP وJavaScript وPython، وإعادة المحاولة، وسجل التسليم.

الـ webhook عنوان على خادمك يتصل به مهنارو كلما حدث شيء في شركتك: إضافة موظف، أو الموافقة على إجازة، أو اعتماد الرواتب. فلا تحتاج أن تسأل الـ API عن التغييرات باستمرار.

إضافة webhook

  1. افتح الشركة ← مفاتيح API والـ Webhooks ثم تبويب Webhooks (تحتاج صلاحية إدارة مفاتيح API والـ webhooks).
  2. اختر إضافة webhook واملأ:
    • الاسم، والرابط الذي سيستقبل الرسائل. يجب أن يكون http أو https ويصل إليه الإنترنت: عناوين الشبكات الخاصة وlocalhost مرفوضة.
    • الشكل: JSON (الافتراضي) أو XML.
    • الأحداث: الأحداث التي تُرسل (انظر أدناه).
    • اختيارياً المصادقة بمفتاح API: عندها يرسل مهنارو مع كل رسالة المصادقة الصادرة لذلك المفتاح (Basic أو OAuth 1 أو OAuth 2 أو اسم مستخدم وكلمة مرور).
  3. احفظ. سر التوقيع (whsec_…) يظهر مرة واحدة: انسخه إلى نظامك المستقبِل.
  4. استخدم إرسال اختبار لإرسال حدث test فوراً ورؤية الرد في سجل التسليم.

تستطيع أيضاً إدارة الـ webhooks بالـ API (POST /api/v1/app/companies/{company}/webhooks، ويرجع data.secret مرة واحدة) أو بأداة MCP manage_webhook، برمز دخول شخص. مفاتيح API لا تدير الـ webhooks.

الشركة التجريبية لا ترسل webhooks أبداً.

الأحداث

المجموعة الحدث يُرسل عندما
الموظف employee.created يُضاف موظف (بأي طريقة: الشاشات، الـ API، MCP، الاستيراد)
الموظف employee.updated يتغير سجل موظف؛ وchanged تذكر الحقول
الهيكل التنظيمي org.branch.created، org.branch.updated، org.branch.deleted يُضاف فرع أو يُعدَّل أو يُحذف
الهيكل التنظيمي org.unit.created، org.unit.updated، org.unit.deleted يُضاف قسم أو وحدة أخرى أو تُعدَّل أو تُحذف
الراتب salary.changed يتغير راتب موظف
الراتب payroll.approved يُعتمد مسير رواتب
الراتب payroll.journal يُعتمد مسير رواتب: قيود اليومية الخاصة به، للمحاسبة
السلفة loan.created، loan.paid تُعطى سلفة، أو تُسدَّد
المعاملة المالية financial_transaction.created، financial_transaction.deleted يُضاف استحقاق أو استقطاع أو يُحذف
إنهاء الخدمة employee.terminated تنتهي خدمة موظف
الطلبات leave.approved، leave.rejected يُبَتّ في إجازة
الطلبات request.approved، request.rejected يُبَتّ في طلب آخر

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 للوقت ثم نقطة ثم نص الرسالة الخام كما وصل تماماً.

  1. اقرأ النص الخام قبل أي تحويل JSON (التحويل وإعادة الترميز يغيّرانه).
  2. احسب HMAC_SHA256(secret, t + "." + body) وقارنه بـ v1 بمقارنة ثابتة الوقت.
  3. ارفض الرسائل التي يبعد t فيها أكثر من 5 دقائق عن الآن، حتى لا تُعاد رسالة قديمة.
  4. ردّ بأي 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 ينشئ سراً جديداً (يظهر مرة واحدة)، والقديم يتوقف فوراً.

آخر تحديث:

ما زلت تحتاج مساعدة؟

فريق الدعم يجيبك بالعربية والإنجليزية.

راسل الدعم

نستخدم ملفات تعريف الارتباط لنفهم كيف يُستخدم الموقع. يمكنك القبول أو الرفض.