الـ Webhooks
بدل أن يستعلم نظامك بشكل دوري، نُرسل إليه طلب POST فور وقوع الحدث.
متاح في خطّتي النمو (نقطة واحدة) والاحتراف (خمس نقاط).
إدارة النقاط
GET /v1/webhooks
POST /v1/webhooks
DELETE /v1/webhooks/{id}
curl -X POST https://fawtara.codexal.co/api/v1/webhooks \
-H "Authorization: Bearer fwt_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/hooks/fawtara",
"events": ["invoice.accepted", "invoice.rejected"]
}'
{
"data": {
"id": "whk_1a2b3c4d5e6f7081",
"object": "webhook_endpoint",
"url": "https://erp.example.com/hooks/fawtara",
"events": ["invoice.accepted", "invoice.rejected"],
"secret": "whsec_9fK2mN7pQ4rS8tU1vW3x",
"active": true
}
}
المفتاح السرّي
secret يُعرض كاملًا مرّة واحدة عند الإنشاء (ومرّة في لوحة التحكم عند الإضافة). خزّنه فورًا — تحتاجه للتحقّق من التوقيع.
الأحداث المتاحة
| الحدث | يُرسل عندما |
|---|---|
invoice.accepted | قُبلت الفاتورة من بوابة الفوترة الوطنية |
invoice.rejected | رُفضت الفاتورة |
invoice.failed | فشل الإرسال لخطأ تقني |
quota.warning | بلوغ 80% من حصة الخطة |
subscription.updated | تغيّر حالة الاشتراك |
payment.succeeded | نجحت عملية دفع إلكتروني |
payment.failed | فشلت عملية دفع إلكتروني |
payment.refunded | استُرجع مبلغ دفعة |
شكل الطلب الوارد إليك
| الترويسة | القيمة |
|---|---|
X-Fawtara-Event | نوع الحدث، مثل invoice.accepted |
X-Fawtara-Delivery | رقم عملية التسليم (يتكرّر عند إعادة المحاولة) |
X-Fawtara-Timestamp | Unix timestamp للتوقيع |
X-Fawtara-Signature | sha256=<hex> |
POST /hooks/fawtara HTTP/1.1
Content-Type: application/json
X-Fawtara-Event: invoice.accepted
X-Fawtara-Timestamp: 1791234567
X-Fawtara-Signature: sha256=5e884898da28047151d0e56f8dc62927…
{
"id": "evt_6d1e8f3a0b2c4759",
"object": "event",
"type": "invoice.accepted",
"created_at": "2026-10-22T23:48:30+00:00",
"mode": "live",
"data": { "id": "inv_…", "object": "invoice", "status": "accepted", "...": "..." }
}
التحقّق من التوقيع
التوقيع هو HMAC-SHA256 للنصّ
timestamp + "." + raw_body بمفتاح النقطة السرّي.
احسبه على الجسم الخام قبل أي تحليل JSON.
<?php
$secret = getenv('FAWTARA_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_FAWTARA_TIMESTAMP'] ?? '';
$given = $_SERVER['HTTP_X_FAWTARA_SIGNATURE'] ?? '';
// ارفض الطلبات الأقدم من خمس دقائق (حماية من إعادة الاستخدام)
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit('stale');
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($raw, true);
// عالج الحدث هنا — ثم أعد 2xx بسرعة
http_response_code(200);
echo 'ok';
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/hooks/fawtara',
express.raw({ type: 'application/json' }), // مهم: الجسم الخام
(req, res) => {
const secret = process.env.FAWTARA_WEBHOOK_SECRET;
const ts = req.get('X-Fawtara-Timestamp') || '';
const sig = req.get('X-Fawtara-Signature') || '';
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send('stale');
}
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(ts + '.' + req.body.toString('utf8'))
.digest('hex');
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString('utf8'));
// عالج الحدث
res.sendStatus(200);
});
import hmac, hashlib, os, time, json
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["FAWTARA_WEBHOOK_SECRET"].encode()
@app.post("/hooks/fawtara")
def hook():
raw = request.get_data()
ts = request.headers.get("X-Fawtara-Timestamp", "")
sig = request.headers.get("X-Fawtara-Signature", "")
if abs(time.time() - int(ts or 0)) > 300:
abort(400)
expected = "sha256=" + hmac.new(
SECRET, f"{ts}.".encode() + raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
event = json.loads(raw)
# عالج الحدث
return "", 200
إعادة المحاولة
نعتبر التسليم ناجحًا عند أي رمز 2xx. غير ذلك نعيد المحاولة وفق التدرّج التالي:
| المحاولة | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| بعد | فورًا | دقيقة | 5 دقائق | 30 دقيقة | ساعتان | 6 ساعات |
بعد المحاولة السادسة تُعلَّم العملية failed، ويمكنك إعادتها يدويًا من لوحة الـ Webhooks.
أفضل الممارسات
- أعد
200بسرعة (أقل من 15 ثانية) وعالِج الحدث في طابور خلفي — مهلتنا 15 ثانية. - اعتبر التسليم قابلًا للتكرار: استخدم
event.idأوdata.idلتجاهل المعالجة المزدوجة. - HTTPS إلزامي، ولا نقبل روابط
http://. - الويب هوك مُكمِّل لا بديل: الردّ المتزامن على
POST /v1/invoicesيحمل الحالة النهائية في معظم الحالات.