توثيق المطوّرين — API بوابة دفع دفتر كاش (Payment Gateway API)

اربط موقعك أو تطبيقك ببوابة الدفع: طلب واحد بيعمل فاتورة ويرجّعلك رابط صفحة دفع جاهزة، والعميل بيحوّل من فودافون كاش أو أورنج كاش أو اتصالات كاش أو وي باي أو إنستاباي، وسيرفرك بيوصله Webhook موقّع أول ما الدفع يتأكد.

Daftar Cash is an Egyptian payment gateway API: create a payment with one request, send the customer to a hosted checkout page, and receive a signed webhook when a Vodafone Cash, Orange Cash, Etisalat Cash, WE Pay or InstaPay transfer is confirmed. Flat monthly subscription, no per-transaction commission. Code examples in cURL, JavaScript, PHP and Python; a WooCommerce plugin is available.

Base URL: https://daftarcash.com

نظرة عامة

البوابة مش بتمسك فلوس عملائك — العميل بيحوّل مباشرة على محفظتك، وهاتف المحفظة (عليه تطبيق دفتر كاش بلس) بيقرأ رسالة التحويل، والنظام بيطابقها مع الفاتورة. دورة الدفع كلها:

سيرفرك يعمل فاتورةبـ POST /api/v1/payments/create/ ويستلم checkout_url.
العميل يدفعبتحوّله على صفحة الدفع المستضافة، يختار المحفظة ويحوّل ويكتب رقم العملية.
التأكيدأوتوماتيك لما رسالة التحويل توصل هاتفك — أو يدوي منك لو العميل رفع لقطة شاشة.
Webhook لسيرفركبحالة completed وموقّع بـ HMAC، والعميل يرجع لـ redirect_url.
كل الطلبات والردود بصيغة JSON وترميز UTF-8، والمبالغ بالجنيه المصري (EGP).

البداية السريعة

  1. اشترك في باقة (أو ابدأ التجربة المجانية) من خطط البوابة.
  2. من لوحة البوابة: ضيف محفظة استقبال، وسجّل دخول بحسابك على تطبيق دفتر كاش بلس على هاتف المحفظة.
  3. من تبويب المطوّرين: انسخ Merchant API Key وSignature Secret Key، وحط رابط الـ Webhook بتاع سيرفرك.
  4. اعمل أول فاتورة:
cURL
curl -X POST https://daftarcash.com/api/v1/payments/create/ \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Key: vc_live_xxxxxxxxxxxxxxxx" \
  -d '{"amount": 450, "merchant_order_id": "1284", "redirect_url": "https://my-store.com/thanks"}'

حوّل العميل على data.checkout_url اللي في الرد، واستنى الـ Webhook.

المصادقة والمفاتيح

عندك التلات مفاتيح دول في لوحة البوابة ← المطوّرين:

المفتاحبيستخدم فيمكانه
Merchant API Key
يبدأ بـ vc_live_
مصادقة طلبات الـ API (إنشاء فاتورة، إعادة إرسال Webhook).سيرفرك بس.
Test API Key
يبدأ بـ vc_test_
نفس طلبات الـ API بالظبط، بس في بيئة التجربة من غير فلوس حقيقية.سيرفرك (بيئة التطوير).
Signature Secret Keyالتحقق من توقيع الـ Webhooks الواردة لسيرفرك.سيرفرك بس — ما بيتبعتش في أي طلب.

ابعت المفتاح العام في أي هيدر من الاتنين:

HTTP
X-Merchant-Key: vc_live_xxxxxxxxxxxxxxxx
# أو
Authorization: Bearer vc_live_xxxxxxxxxxxxxxxx
ما تحطّش المفاتيح في كود الواجهة (المتصفح أو تطبيق الموبايل). اعمل الفاتورة من سيرفرك دايمًا. لو مفتاح اتسرّب، جدّده فورًا من زرار "تجديد المفاتيح" — المفتاح القديم بيبطل على طول.

بيئة التجربة (Sandbox)

جرّب الربط كله من أوله لآخره من غير ما تحوّل فلوس حقيقية: ابعت نفس الطلبات بمفتاح vc_test_ بدل vc_live_.

  • الفاتورة بتتعمل عادي وبترجع "test_mode": true، ومش بتتحسب من حد حجم المعاملات في باقتك.
  • صفحة الدفع (checkout_url) مابتعرضش رقم محفظتك، وبتعرض بداله زرارين: محاكاة دفع ناجح ومحاكاة دفع فاشل.
  • الدفع الناجح بيبعت Webhook عادي، متوقّع بنفس المفتاح السري، وفيه "test_mode": true، وبيحوّل العميل على redirect_url.
  • الدفع الفاشل بيقفل الفاتورة failed من غير Webhook، زي الدفع الحقيقي بالظبط.
  • فواتير التجربة بتظهر في لوحتك بعلامة "تجربة"، ومش بتدخل في الإحصائيات.
  • مفتاح التجربة شغّال حتى لو اشتراك البوابة خلص، عشان تقدر تكمّل تطوير.
في الـ Webhook، اتأكد من test_mode: لو true ما تعلّمش الطلب مدفوع في متجرك الحقيقي. قبل الإطلاق بدّل المفتاح لـ vc_live_.

إنشاء فاتورة دفع

POST/api/v1/payments/create/ بمفتاح API

الحقول

الحقلالنوعالوصف
amountإجباريnumberالمبلغ بالجنيه، أكبر من صفر (مثال 450 أو 199.50). العميل لازم يحوّل المبلغ ده أو أكتر.
order_totalاختياريnumberإجمالي الطلب لو amount ده عربون والباقي هيتدفع كاش عند الاستلام. لازم يكون أكبر من amount.
merchant_order_idاختياريstringرقم الطلب عندك (حتى 100 حرف). بيرجعلك في الـ Webhook وبيظهر للعميل في صفحة الدفع.
redirect_urlاختياريstring (URL)العميل بيتحوّل عليه تلقائيًا بعد نجاح الدفع (صفحة "شكرًا لطلبك").
cancel_urlاختياريstring (URL)العميل بيروحه لو قفل صفحة دفع منتهية أو مرفوضة. لو ما بعتّوش، بيرجع لجذر موقعك (دومين redirect_url).
customer_phoneاختياريstringرقم موبايل العميل المصري. للعرض والإيصالات فقط — المطابقة بتتم برقم العملية اللي العميل بيكتبه في صفحة الدفع.
customer_emailاختياريstringإيميل العميل — لو بعتّه، بيوصله إيصال دفع تلقائي بعد اكتمال الفاتورة.

مثال

curl -X POST https://daftarcash.com/api/v1/payments/create/ \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Key: $DAFTAR_API_KEY" \
  -d '{
    "amount": 450,
    "merchant_order_id": "1284",
    "redirect_url": "https://my-store.com/orders/1284/thanks",
    "cancel_url": "https://my-store.com/cart"
  }'
const res = await fetch("https://daftarcash.com/api/v1/payments/create/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Merchant-Key": process.env.DAFTAR_API_KEY,
  },
  body: JSON.stringify({
    amount: 450,
    merchant_order_id: "1284",
    redirect_url: "https://my-store.com/orders/1284/thanks",
    cancel_url: "https://my-store.com/cart",
  }),
});
const body = await res.json();
if (!body.success) throw new Error(body.message);

// Express: send the customer to the hosted checkout page
response.redirect(body.data.checkout_url);
<?php
$ch = curl_init("https://daftarcash.com/api/v1/payments/create/");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "Content-Type: application/json",
        "X-Merchant-Key: " . getenv("DAFTAR_API_KEY"),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        "amount"            => 450,
        "merchant_order_id" => "1284",
        "redirect_url"      => "https://my-store.com/orders/1284/thanks",
        "cancel_url"        => "https://my-store.com/cart",
    ]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);

if (empty($body["success"])) {
    throw new Exception($body["message"] ?? "Payment gateway error");
}
header("Location: " . $body["data"]["checkout_url"]);
exit;
import os
import requests

res = requests.post(
    "https://daftarcash.com/api/v1/payments/create/",
    headers={"X-Merchant-Key": os.environ["DAFTAR_API_KEY"]},
    json={
        "amount": 450,
        "merchant_order_id": "1284",
        "redirect_url": "https://my-store.com/orders/1284/thanks",
        "cancel_url": "https://my-store.com/cart",
    },
    timeout=15,
)
body = res.json()
if not body.get("success"):
    raise RuntimeError(body.get("message"))

checkout_url = body["data"]["checkout_url"]  # redirect the customer here

الرد 201 Created

JSON
{
  "success": true,
  "message": "Payment invoice created successfully.",
  "data": {
    "payment_id": "3f6c1a2e-8b1d-4e0a-9c55-2d7f0b9e4a11",
    "amount": 450.0,
    "status": "pending",
    "checkout_url": "https://daftarcash.com/checkout/hosted/3f6c1a2e-8b1d-4e0a-9c55-2d7f0b9e4a11/",
    "expires_at": "2026-09-27T14:05:12.481Z",
    "test_mode": false,
    "payment_type": "full",
    "order_total": 450.0,
    "remaining_on_delivery": 0.0
  }
}

احفظ payment_id مع الطلب عندك — ده المعرّف اللي بيرجع في الـ Webhook وفي استعلام الحالة. رابط الدفع صالح 24 ساعة (expires_at).

Idempotency: لو نادَيت /payments/create/ بنفس merchant_order_id تاني (retry بسبب مشكلة شبكة مثلًا) ولسه الفاتورة الأولى pending وسارية، هترجعلك نفس payment_id وcheckout_url (كود 200 مش 201) بدل ما تتعمل فاتورة جديدة — بشرط يكون نفس amount وorder_total. لو المبلغ اتغيّر، بتتعمل فاتورة جديدة بالمبلغ الجديد (201) والقديمة بتتقفل ويوصلك لها payment.failed.

عربون الدفع عند الاستلام

بدل ما تشحن طلبات الدفع عند الاستلام وتتفاجئ إن العميل رفضها، خلّيه يدفع عربون صغير بالمحفظة يأكّد بيه جدّيته، والباقي يدفعه كاش للمندوب.

  • ابعت amount = العربون، وorder_total = إجمالي الطلب.
  • صفحة الدفع بتوضّح للعميل: الإجمالي، والعربون اللي هيحوّله دلوقتي، والباقي عند الاستلام.
  • التأكيد التلقائي وحد الباقة بيتحسبوا على العربون بس — ده اللي بيعدّي على محفظتك.
  • الـ Webhook بيوصلك بـ "status": "completed" و"payment_type": "deposit"، وremaining_on_delivery = المبلغ اللي المندوب يحصّله. ما تعلّمش الطلب مدفوع بالكامل — علّمه "مؤكَّد، الباقي عند الاستلام".
  • من غير كود: فعّل "عربون الدفع عند الاستلام" من إعدادات لوحة البوابة، وبعدين في رابط الدفع السريع اكتب العربون في "المبلغ" والإجمالي في "إجمالي الطلب".
  • الـ API مش مربوط بالزرار ده — order_total بيشتغل دايمًا لو بعته.
  • بلجن WooCommerce بيعمل ده لوحده: فعّل "عربون الدفع عند الاستلام" من إعداداته وحدد نسبة أو مبلغ ثابت.
JSON
{
  "amount": 150,
  "order_total": 1200,
  "merchant_order_id": "1290"
}

صفحة الدفع المستضافة

الـ checkout_url صفحة جاهزة باسم متجرك، شغّالة على الموبايل، ومش محتاج تبني أي واجهة:

  1. العميل بيختار المحفظة اللي هيحوّل منها — والصفحة بتعرض محافظ الاستقبال النشطة عندك (بالتبادل بينهم).
  2. بيحوّل المبلغ ويكتب رقم العملية من رسالة تأكيد التحويل.
  3. الصفحة بتستنى التأكيد التلقائي لمدة بتحددها من لوحة البوابة (دقيقتين، 3 دقائق، 5 دقائق، 10 دقائق، 15 دقيقة، 30 دقيقة، ساعة).
  4. لو اتأكد: رسالة نجاح وتحويل تلقائي لـ redirect_url.
  5. لو المدة خلصت: العميل يصحّح رقم العملية أو يرفع لقطة شاشة للتحويل، وإنت تأكد أو ترفض من تبويب المراجعة في لوحتك — والتأكيد اليدوي بيبعت نفس الـ Webhook.
ما تعتمدش على رجوع العميل لـ redirect_url عشان تأكد الطلب — العميل ممكن يقفل الصفحة قبل التحويل. الـ Webhook (أو استعلام الحالة) هو مصدر الحقيقة الوحيد.

تقدر تبعت العميل للرابط من صفحة الطلب، أو تبعته في رسالة واتساب/SMS — الرابط شغّال من أي جهاز.

متابعة حالة الفاتورة

GET/api/v1/payments/status/{payment_id}/ بدون مفتاح

مفيد كاحتياطي لو الـ Webhook ما وصلش، أو لو عايز تعرض حالة الطلب في صفحتك.

JSON
{
  "success": true,
  "data": {
    "payment_id": "3f6c1a2e-8b1d-4e0a-9c55-2d7f0b9e4a11",
    "status": "completed",
    "awaiting_merchant_review": false,
    "amount": 450.0,
    "merchant_order_id": "1284",
    "merchant_name": "noor_store",
    "redirect_url": "https://my-store.com/orders/1284/thanks",
    "created_at": "2026-09-26T14:05:12.481Z",
    "expires_at": "2026-09-27T14:05:12.481Z",
    "test_mode": false,
    "payment_type": "full",
    "order_total": 450.0,
    "remaining_on_delivery": 0.0
  }
}

الحالات

statusالمعنى
pendingمستنية الدفع. لو awaiting_merchant_review = true، العميل رفع لقطة شاشة ومستنية قرارك (ومش بتنتهي لوحدها).
completedالدفع اتأكد (تلقائي أو يدوي). جهّز الطلب.
expiredالرابط خلص (24 ساعة) من غير دفع.
failedإنت رفضت لقطة الشاشة من لوحة المراجعة، أو ألغيتها عبر API الإلغاء.
refundedالتاجر علّمها مستردة يدويًا (رجّع الفلوس لعميله بنفسه) عبر API الاسترداد أو لوحة البوابة.

إلغاء فاتورة

POST/api/v1/payments/{payment_id}/cancel/ بمفتاح API

لما العميل يلغي الطلب من متجرك قبل ما يدفع. بيشتغل بس على فاتورة لسه pending، وبيقفلها failed ويبعتلك Webhook بـ event: "payment.failed" — نفس الحدث اللي بيوصلك لما ترفض لقطة شاشة، فأي كود عندك بيتعامل معاه أصلًا هيلغي الطلب صح من غير أي تعديل.

cURL
curl -X POST https://daftarcash.com/api/v1/payments/3f6c1a2e-8b1d-4e0a-9c55-2d7f0b9e4a11/cancel/ \
  -H "X-Merchant-Key: $DAFTAR_API_KEY"

لو الفاتورة خلاص completed أو expired أو failed، هترجع 400 ("Cannot cancel an invoice in '…' state").

استرداد فاتورة

POST/api/v1/payments/{payment_id}/refund/ بمفتاح API
البوابة مابترجّعش فلوس فعليًا — المبلغ أصلًا في محفظتك، مش عندنا. الـ API ده بس بيسجّل إنك رجّعت الفلوس لعميلك بنفسك، ويبعت Webhook يحدّث حالة الطلب عندك.

بيشتغل بس على فاتورة completed، وبيقفلها refunded ويبعتلك Webhook بـ event: "payment.refunded". نفس الميزة متاحة يدويًا من لوحة البوابة ← سجلّ طلبات الدفع (زرار "رد المبلغ" جنب أي فاتورة مكتملة).

cURL
curl -X POST https://daftarcash.com/api/v1/payments/3f6c1a2e-8b1d-4e0a-9c55-2d7f0b9e4a11/refund/ \
  -H "X-Merchant-Key: $DAFTAR_API_KEY"

لو الفاتورة مش completed (لسه pending، أو أصلًا expired/failed/refunded)، هترجع 400 ("Only completed invoices can be refunded").

فواتير دورية

POST/api/v1/recurring/create/ بمفتاح API

لاشتراكات عملائك (نادي، دروس، خدمة شهرية…): بتعرّف الاشتراك مرة واحدة، وإحنا بنولّد فاتورة جديدة تلقائيًا كل دورة ونبعت لعميلك رابط الدفع على إيميله، ونذكّره مرة لو ما دفعش خلال 24 ساعة. مفيش خصم تلقائي — العميل بيحوّل بنفسه كل دورة، والدفع بيتأكد زي أي فاتورة، وبيوصلك payment.completed فيه recurring_plan_id وmerchant_order_id بصيغة REC<plan_id>-<رقم الدورة> عشان تمدّ خدمة عميلك.

الحقلالنوعالوصف
amountnumberمبلغ كل دورة بالجنيه.
intervalstringweekly أو monthly. الشهري بيحافظ على يوم الشهر (31 يناير ← 28 فبراير ← 31 مارس).
customer_emailstringمطلوب — بيوصله رابط كل فاتورة.
customer_name / customer_phone / label / redirect_urlstringاختياري.
start_atISO 8601اختياري — موعد أول فاتورة (الافتراضي: دلوقتي).
cURL
curl -X POST https://daftarcash.com/api/v1/recurring/create/ \
  -H "X-Merchant-Key: $DAFTAR_API_KEY" -H "Content-Type: application/json" \
  -d '{"amount": 300, "interval": "monthly", "customer_email": "ahmed@example.com", "label": "اشتراك الجيم"}'

الرد 201 فيه plan_id. بعدها: GET /api/v1/recurring/<plan_id>/ لحالة الاشتراك وآخر 5 فواتير، وPOST /api/v1/recurring/<plan_id>/pause|resume|cancel/. الاستئناف بعد إيقاف مؤقت بيتخطى الدورات اللي فاتت.

الاشتراك بيتوقف تلقائيًا بعد 3 فواتير متتالية بدون دفع، وحدّه الأقصى حسب باقتك (30 / 150 / 600). مش متاح في بيئة التجربة.

الـ Webhooks

البوابة بتبعت POST لرابط الـ Webhook اللي حاطه في لوحة البوابة ← المطوّرين عند أي تغيير نهائي في حالة الفاتورة: دُفعت (payment.completed)، رفض التاجر إثباتها (payment.failed)، خلصت مهلتها من غير دفع (payment.expired)، أو اتعلّمت مستردة يدويًا (payment.refunded) — مش لما تُدفع بس. فرّق بينهم بحقل event (استخدم ده مش status وحده في كود التكامل عندك).

الهيدرالقيمة
Content-Typeapplication/json
X-VodaCash-SignatureHMAC-SHA256 (hex) لجسم الطلب الخام، بمفتاح Signature Secret Key.
X-VodaCash-Delivery-Idرقم ثابت لكل إشعار (بيتكرر في إعادات المحاولة لنفس الإشعار) — استخدمه مع event وpayment_id لمنع المعالجة المزدوجة.
X-VodaCash-Attemptرقم المحاولة (1 للأولى).
JSON
{
  "event": "payment.completed",
  "payment_id": "3f6c1a2e-8b1d-4e0a-9c55-2d7f0b9e4a11",
  "merchant_order_id": "1284",
  "amount": 450.0,
  "status": "completed",
  "verified_transaction_id": "023639006701",
  "test_mode": false,
  "recurring_plan_id": null,
  "payment_type": "full",
  "order_total": 450.0,
  "remaining_on_delivery": 0.0,
  "timestamp": "2026-09-26T14:09:47.120Z"
}
  • event: واحد من payment.completed، payment.failed، payment.expired، payment.refunded — نفس قيمة status بالظبط بس بصيغة اسم حدث ثابتة، مفيدة لو حبيت تعمل switch بدل مقارنة نص.
  • verified_transaction_id: رقم العملية الحقيقي من رسالة التحويل — مفيد للمراجعة المحاسبية (فاضي في payment.failed/payment.expired).
  • payment_type وorder_total وremaining_on_delivery: لو payment_type قيمته "deposit" يبقى العميل دفع العربون بس، والباقي هيتدفع عند الاستلام.
  • test_mode: true لو الفاتورة اتعملت بمفتاح التجربة — ما تعتبرهاش دفعة حقيقية.
  • recurring_plan_id: رقم الاشتراك الدوري اللي ولّد الفاتورة، أو null لأي فاتورة عادية.
  • رد بأي كود 2xx خلال 10 ثواني. لو ما ردّيتش أو رجّعت خطأ، البوابة بتعيد المحاولة: 3 محاولات متتالية سريعة، وبعدها إعادة تلقائية بعد 5 دقايق، 30 دقيقة، ساعتين، 6 ساعات، 24 ساعة (8 محاولات إجمالًا). بعد كده تقدر تعيد الإرسال يدويًا من هنا. كل إعادة بتحمل نفس event وstatus بتوع الإشعار الأصلي، بـ timestamp جديد.
  • رابط الـ Webhook لازم يكون https:// على عنوان عام — الروابط اللي بتشاور على عنوان داخلي أو محلي (localhost، 127.0.0.1، 192.168.x.x…) أو فيها اسم مستخدم وكلمة مرور بتتقفل. للتجربة المحلية استخدم نفق مثل ngrok. البوابة مابتتبعش redirects (3xx بتتحسب فشل) فاحط الرابط النهائي مباشرة.
  • الحماية من إعادة الإرسال المتأخر: ارفض أي إشعار timestamp بتاعه أقدم من 5 دقايق (إعادات المحاولة بتتبعت بـ timestamp جديد فمش هتتأثر).
  • كل محاولة بتتسجل في سجل إرسال الـ Webhooks في لوحتك (كود الرد + الرد نفسه).
  • نفس الإشعار ممكن يوصلك أكتر من مرة (إعادة محاولة أو إرسال يدوي) — خلي المعالجة idempotent: لو الطلب متعلّم مدفوع أو ملغي خلاص بنفس الحالة، تجاهل.

التحقق من التوقيع

احسب HMAC-SHA256 على جسم الطلب الخام كما وصل (مش بعد ما تعمل له parse وتعيد تحويله لـ JSON — أي اختلاف في المسافات بيغيّر التوقيع)، وقارنه بالهيدر بمقارنة آمنة ضد التوقيت.

const crypto = require("crypto");
const express = require("express");
const app = express();

// keep the raw body: the signature is computed over the exact bytes
app.post("/webhooks/daftar-cash", express.raw({ type: "application/json" }), async (req, res) => {
  const expected = crypto
    .createHmac("sha256", process.env.DAFTAR_WEBHOOK_SECRET)
    .update(req.body)                      // Buffer
    .digest("hex");
  const received = req.get("X-VodaCash-Signature") || "";

  const ok = received.length === expected.length &&
             crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.status(401).send("invalid signature");

  const event = JSON.parse(req.body);
  if (event.test_mode) return res.sendStatus(200); // Sandbox: never touch a real order

  if (event.event === "payment.completed") {
    await markOrderPaid(event.merchant_order_id, event.payment_id, event.verified_transaction_id);
  } else if (event.event === "payment.failed" || event.event === "payment.expired") {
    await cancelOrder(event.merchant_order_id);
  }
  res.sendStatus(200);
});
<?php
$rawBody  = file_get_contents("php://input");
$received = $_SERVER["HTTP_X_VODACASH_SIGNATURE"] ?? "";
$expected = hash_hmac("sha256", $rawBody, getenv("DAFTAR_WEBHOOK_SECRET"));

if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit("invalid signature");
}

$event = json_decode($rawBody, true);
if (empty($event["test_mode"])) {  // skip Sandbox events
    if (($event["event"] ?? "") === "payment.completed") {
        mark_order_paid($event["merchant_order_id"], $event["payment_id"], $event["verified_transaction_id"]);
    } elseif (in_array($event["event"] ?? "", array("payment.failed", "payment.expired"), true)) {
        cancel_order($event["merchant_order_id"]);
    }
}
http_response_code(200);
import hashlib
import hmac
import json
import os

from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def daftar_cash_webhook(request):
    expected = hmac.new(os.environ["DAFTAR_WEBHOOK_SECRET"].encode(), request.body, hashlib.sha256).hexdigest()
    received = request.headers.get("X-VodaCash-Signature", "")
    if not hmac.compare_digest(expected, received):
        return HttpResponseForbidden("invalid signature")

    event = json.loads(request.body)
    if not event.get("test_mode"):  # skip Sandbox events
        if event.get("event") == "payment.completed":
            mark_order_paid(event["merchant_order_id"], event["payment_id"], event["verified_transaction_id"])
        elif event.get("event") in ("payment.failed", "payment.expired"):
            cancel_order(event["merchant_order_id"])
    return HttpResponse(status=200)
قبل ما تعلّم الطلب مدفوع، اتأكد إن amount في الإشعار بيساوي مبلغ الطلب عندك، وإن payment_id هو نفس اللي حفظته وقت إنشاء الفاتورة.

إعادة إرسال Webhook

POST/api/v1/payments/webhook-retry/ بمفتاح API

بيعيد إرسال إشعار فاتورة completed أو failed أو expired لرابط الـ Webhook الحالي (محاولة واحدة، ويرجعلك نتيجتها). نفس اللي بيعمله زرار "إعادة إرسال" في سجل الـ Webhooks.

cURL
curl -X POST https://daftarcash.com/api/v1/payments/webhook-retry/ \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Key: $DAFTAR_API_KEY" \
  -d '{"payment_id": "3f6c1a2e-8b1d-4e0a-9c55-2d7f0b9e4a11"}'

# => {"success": true, "message": "Webhook delivered successfully",
#     "details": {"status_code": 200, "response": "OK"}}

الأخطاء والحدود

أي رد فيه "success": false معاه message بيوضّح السبب:

الكودالسببتعمل إيه
400JSON غير صالح، أو amount ناقص/صفر/مش رقم.صلّح الطلب.
401مفتاح الـ API ناقص أو غلط.راجع الهيدر، أو المفتاح اتجدّد.
403اشتراك البوابة على حسابك منتهي أو مش مفعّل.جدّد الباقة من لوحة البوابة.
404الفاتورة مش موجودة (أو مش بتاعة حسابك).راجع payment_id.
429 + "error_code": "quota_exceeded"الفاتورة هتتخطى حد حجم المعاملات الشهري لباقتك (المبالغ المدفوعة فعلاً + هامش سماح 10%).رقّي الباقة — الحد بيتصفّر مع كل دورة (30 يوم).
429تخطيت حد عدد الطلبات.استنى دقيقة وحاول تاني (استخدم backoff).

حدود عدد الطلبات

المسارالحد
POST /api/v1/payments/create/30 طلب في الدقيقة لكل حساب.
POST /api/v1/payments/webhook-retry/10 طلبات في الدقيقة لكل حساب.

WooCommerce

صفحة الإضافة: المتطلبات وخطوات التركيب والأسئلة الشائعة ←

لو متجرك على WooCommerce مش محتاج تكتب أي كود — البلجن بيعمل الفاتورة ويحوّل العميل ويستقبل الـ Webhook ويحدّث حالة الطلب لوحده.

  1. حمّل البلجن وارفعه من الإضافات ← أضف جديد ← رفع إضافة وفعّله.
  2. من WooCommerce ← الإعدادات ← المدفوعات ← دفتر كاش: فعّل البوابة وحط Merchant API Key وSignature Secret Key.
  3. البلجن بيعرضلك رابط الـ Webhook (https://your-store.com/wc-api/vodacash_webhook/) — الصقه في لوحة البوابة ← المطوّرين.
من الإصدار 1.1.2، البلجن بيبعت إيميل العميل (Billing Email) تلقائيًا مع الفاتورة — لو عندك إصدار أقدم، نزّل النسخة الجديدة وحدّثها عشان عميلك ياخد إيصال دفع تلقائي.

أسئلة شائعة

إيه صيغة الطلبات وعنوان إنشاء فاتورة الدفع؟

بتبعت POST على /api/v1/payments/create/ بصيغة JSON وترميز UTF-8، والمبالغ بالجنيه المصري (EGP)، وبيرجعلك checkout_url تحوّل عليه العميل لصفحة الدفع المستضافة.

إزاي بتتم المصادقة على طلبات الـ API؟

بتبعت Merchant API Key (بيبدأ بـ vc_live_) في هيدر X-Merchant-Key أو Authorization: Bearer، من سيرفرك بس. ما تحطّش المفاتيح في كود المتصفح أو تطبيق الموبايل.

أقدر أجرّب الربط من غير فلوس حقيقية؟

أيوه. بمفتاح التجربة (بيبدأ بـ vc_test_) الفواتير بتتعمل بوضع test_mode، وبيظهر في صفحة الدفع زرارين لمحاكاة دفع ناجح أو فاشل، وبيوصلك Webhook عادي فيه علامة التجربة.

إزاي أتأكد إن الـ Webhook جاي من البوابة فعلًا؟

كل Webhook موقّع بتوقيع HMAC باستخدام Signature Secret Key. بتتحقق من الهيدر X-VodaCash-Signature على الجسم الخام قبل ما تعلّم الطلب مدفوع، والخطوات بالأكواد في قسم التحقق من التوقيع.

إيه حدود عدد الطلبات على الـ API؟

إنشاء الفاتورة 30 طلب في الدقيقة لكل حساب، وإعادة إرسال Webhook 10 طلبات في الدقيقة. لو اتخطيت الحد بيرجع الرد 429 وتستنى دقيقة وتحاول تاني.

لو متجري WooCommerce محتاج أكتب كود؟

لأ. بلجن WooCommerce بيعمل الفاتورة ويحوّل العميل ويستقبل الـ Webhook ويحدّث حالة الطلب لوحده.

اقرأ أيضًا: إضافة WooCommerce · أسعار البوابة · البوابة بدون سجل تجاري

قبل الإطلاق

  • المفاتيح في متغيرات بيئة على السيرفر — مش في الكود ولا في الواجهة.
  • الـ Webhook بيتحقق من X-VodaCash-Signature على الجسم الخام، وبيقارن المبلغ وpayment_id.
  • معالجة الـ Webhook idempotent، وبترد 200 بسرعة (أي شغل تقيل يتعمل بعد الرد).
  • فيه محفظة استقبال نشطة، وهاتفها عليه تطبيق دفتر كاش بلس شغّال في الخلفية.
  • جرّبت الربط في بيئة التجربة (دفع ناجح وفاشل)، وبعدين بدّلت لمفتاح vc_live_.
  • جرّبت دفعة حقيقية صغيرة من أوّلها لآخرها، وشفت الإشعار في سجل الـ Webhooks.
  • حددت مدة انتظار التأكيد المناسبة لعملائك، وبتتابع تبويب المراجعة.
محتاج مساعدة في الربط؟ كلّمنا على info@daftarcash.com.