ف فواترا

الـ 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-TimestampUnix timestamp للتوقيع
X-Fawtara-Signaturesha256=<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. غير ذلك نعيد المحاولة وفق التدرّج التالي:

المحاولة123456
بعدفورًادقيقة5 دقائق30 دقيقةساعتان6 ساعات

بعد المحاولة السادسة تُعلَّم العملية failed، ويمكنك إعادتها يدويًا من لوحة الـ Webhooks.

أفضل الممارسات

  • أعد 200 بسرعة (أقل من 15 ثانية) وعالِج الحدث في طابور خلفي — مهلتنا 15 ثانية.
  • اعتبر التسليم قابلًا للتكرار: استخدم event.id أو data.id لتجاهل المعالجة المزدوجة.
  • HTTPS إلزامي، ولا نقبل روابط http://.
  • الويب هوك مُكمِّل لا بديل: الردّ المتزامن على POST /v1/invoices يحمل الحالة النهائية في معظم الحالات.