ITSkillsCenter
الأعمال الرقمية

دفع Wave عند التوصيل: تكامل API + Webhooks: درس 2026

5 دقائق للقراءة

📍 المقالة الرئيسية للمجموعة: Stack لوجستية 2026.

40% من زبائن إفريقيا الغربية يفضلون Wave Mobile Money للدفع. أرخص (1% عمولة)، فوري، آمن، مدمج في الهاتف. دمج Wave في tunnel checkout متجر + تطبيق ساعي = تخفيض refus payment، إدارة COD أبسط، reconciliation يومية مؤتمتة. هذا الدرس يفصل التكامل الكامل: Wave Business API، QR code dynamique، webhooks signed، reconciliation.

المتطلبات

حساب Wave Business (تسجيل على business.wave.com، KYC 3-5 أيام). متجر إلكتروني (WooCommerce، Next.js، QloApps). تطبيق ساعي. backend Node.js/Bun. المستوى المتوقع: متقدم. الوقت المقدر: 4-6 ساعات.

الخطوة 1 — الحصول على API credentials

business.wave.com → Developers → API Keys. تستلم:

  • WAVE_API_KEY: للمصادقة API.
  • WAVE_WEBHOOK_SECRET: للتحقق من signature webhooks.
  • WAVE_BUSINESS_ID: معرف نشاطك التجاري.

خزن في Vaultwarden + متغيرات بيئة. لا تكشف API key في front-end أو git.

الخطوة 2 — إنشاء checkout session

عند checkout، backend ينشئ Wave session ويعيد URL إعادة توجيه. العميل ينقر، يفتح Wave app أو موقع، يدفع، يعود.

// backend/src/payment/wave.ts
import { createHmac } from 'node:crypto';

export async function createWaveCheckoutSession(orderId: string, amount: number, customerPhone: string) {
  const response = await fetch('https://api.wave.com/v1/checkout/sessions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.WAVE_API_KEY}`,
      'Content-Type': 'application/json',
      'idempotency-key': `order-${orderId}`
    },
    body: JSON.stringify({
      amount: amount.toString(),
      currency: 'XOF',
      success_url: `https://shop.com/order/${orderId}/success`,
      error_url: `https://shop.com/order/${orderId}/error`,
      client_reference: orderId,
      restrict_payer_mobile: customerPhone  // اختياري: قيد على رقم محدد
    })
  });
  
  if (!response.ok) {
    throw new Error(`Wave error: ${await response.text()}`);
  }
  
  const session = await response.json();
  // {
  //   id: 'cos-1...',
  //   wave_launch_url: 'https://pay.wave.com/c/...',
  //   amount: '5000',
  //   currency: 'XOF',
  //   status: 'open'
  // }
  
  return session;
}

الخطوة 3 — Frontend تكامل

زر «دفع بـ Wave» يستدعي backend، يحول العميل إلى wave_launch_url.

// app/checkout/page.tsx
async function payWithWave() {
  const res = await fetch('/api/checkout', {
    method: 'POST',
    body: JSON.stringify({
      items: cart,
      paymentMethod: 'wave',
      customerPhone
    })
  });
  
  const { wave_launch_url } = await res.json();
  
  // إعادة توجيه إلى Wave
  window.location.href = wave_launch_url;
  // أو فتح في tab جديد لـ desktop:
  // window.open(wave_launch_url, '_blank');
}

الخطوة 4 — Webhook استقبال التأكيدات

Wave يرسل webhook عندما الدفع ناجح أو فشل. التحقق من signature إلزامي لتجنب spam.

// backend/src/webhooks/wave.ts
import express from 'express';

app.post('/api/webhooks/wave', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.header('Wave-Signature');
  const payload = req.body;  // raw bytes
  
  // التحقق HMAC
  const expectedSig = createHmac('sha256', process.env.WAVE_WEBHOOK_SECRET!)
    .update(payload)
    .digest('hex');
  
  if (signature !== expectedSig) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = JSON.parse(payload.toString());
  // {
  //   id: 'evt-1...',
  //   type: 'checkout.session.completed',
  //   data: {
  //     id: 'cos-1...',
  //     amount: '5000',
  //     currency: 'XOF',
  //     client_reference: 'order-123',
  //     payment_status: 'succeeded',
  //     payer_phone: '+221701234567'
  //   }
  // }
  
  switch (event.type) {
    case 'checkout.session.completed':
      await handlePaymentSuccess(event.data);
      break;
    case 'checkout.session.payment_failed':
      await handlePaymentFailure(event.data);
      break;
  }
  
  res.status(200).send('OK');
});

async function handlePaymentSuccess(session: any) {
  await sql`
    UPDATE orders 
    SET status = 'paid', 
        payment_method = 'wave',
        paid_at = now(),
        wave_session_id = ${session.id}
    WHERE id = ${session.client_reference}
  `;
  
  // إشعار العميل
  await sendSMS(session.payer_phone, 'تم استلام دفعتك. طلبك في طور التحضير.');
  
  // إرسال إلى الساعي (إذا التوصيل تلقائي)
  await assignDeliveryToCourier(session.client_reference);
}

الخطوة 5 — حالة Cash on Delivery

40% من الزبائن يدفعون نقداً عند التوصيل. الساعي يقبض، يعطي إيصال. التطبيق يسجل المبلغ، reconciliation يومية.

// تطبيق ساعي
const confirmCashPayment = async (deliveryId: string, amount: number) => {
  await db.deliveries.update(deliveryId, {
    status: 'delivered',
    paymentMethod: 'cash',
    cashAmount: amount,
    syncStatus: 'pending',
    updatedAt: Date.now()
  });
  
  await db.syncQueue.add({
    type: 'cash_payment',
    data: { deliveryId, amount, courierId: getMe().id },
    attempts: 0,
    createdAt: Date.now()
  });
};

الخطوة 6 — QR Code للساعي

الساعي يولد QR code dynamique للتوصيل، العميل يمسح، يدفع مباشرة على الساعي. يتطلب Wave Personal API (تحويل بين Wave wallets).

// تطبيق ساعي - توليد QR
import QRCode from 'qrcode';

const generatePaymentQR = async (deliveryId: string, amount: number) => {
  // إنشاء payment link قصير
  const res = await fetch('/api/wave/payment-link', {
    method: 'POST',
    body: JSON.stringify({ deliveryId, amount })
  });
  const { paymentUrl } = await res.json();
  
  // تحويل إلى QR code
  const qrDataUrl = await QRCode.toDataURL(paymentUrl);
  return qrDataUrl;
};

// عرض QR
<img src={qrDataUrl} alt="مسح للدفع" />
<p>المبلغ: {amount} XOF</p>
<p>أو ادفع نقداً</p>

الخطوة 7 — Reconciliation يومية

كل ليلة، script يقارن الدفعات Wave مع الطلبات في DB. يكشف الانحرافات (دفعة Wave دون طلب، طلب نقداً دون دفعة، إلخ).

// scripts/wave-reconciliation.ts
async function reconcile(date: string) {
  // جلب كل الـ Wave transactions لليوم
  const waveTransactions = await fetch(
    `https://api.wave.com/v1/transactions?date=${date}`,
    { headers: { 'Authorization': `Bearer ${process.env.WAVE_API_KEY}` } }
  ).then(r => r.json());
  
  // جلب الطلبات Wave لليوم
  const orders = await sql`
    SELECT * FROM orders 
    WHERE payment_method = 'wave' AND DATE(paid_at) = ${date}
  `;
  
  // التطابق
  const matches = [];
  const unmatched = { wave: [], orders: [] };
  
  for (const tx of waveTransactions) {
    const order = orders.find(o => o.wave_session_id === tx.session_id);
    if (order) matches.push({ tx, order });
    else unmatched.wave.push(tx);
  }
  
  for (const order of orders) {
    if (!matches.find(m => m.order.id === order.id)) {
      unmatched.orders.push(order);
    }
  }
  
  // تقرير email
  await sendReport({
    date,
    total_matched: matches.length,
    total_amount: matches.reduce((s, m) => s + m.tx.amount, 0),
    unmatched
  });
}

الخطوة 8 — Refunds

طلب ملغى = refund. Wave Refunds API.

async function refundOrder(orderId: string, reason: string) {
  const order = await getOrder(orderId);
  if (order.payment_method !== 'wave') return;
  
  const res = await fetch('https://api.wave.com/v1/refunds', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.WAVE_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      session_id: order.wave_session_id,
      amount: order.amount.toString(),
      reason
    })
  });
  
  if (res.ok) {
    await sql`UPDATE orders SET status = 'refunded' WHERE id = ${orderId}`;
    await sendSMS(order.customer_phone, 'تم استرداد المبلغ بالكامل في محفظة Wave الخاصة بك.');
  }
}

الخطوة 9 — تخصيص Cash float للساعي

الساعي يبدأ يومه بـ 50,000 XOF نقد لإعادة الفرق. التطبيق يتتبع:

// schema courier
ALTER TABLE couriers ADD cash_float integer DEFAULT 50000;

// تحديث في كل توصيل cash
async function updateCashFloat(courierId: string, amountReceived: number, change: number) {
  await sql`
    UPDATE couriers 
    SET cash_float = cash_float + ${amountReceived} - ${change}
    WHERE id = ${courierId}
  `;
}

الخطوة 10 — Reporting و KPIs

Dashboard يعرض: % الدفعات Wave vs Cash، متوسط المبلغ، معدل الفشل، reconciliation status.

SELECT 
  payment_method,
  COUNT(*) as count,
  SUM(amount) as total,
  AVG(amount) as avg
FROM orders
WHERE created_at > now() - interval '30 days'
GROUP BY payment_method;

-- نتيجة نموذجية:
-- wave  | 1850 | 9,250,000 | 5,000
-- cash  | 1230 | 6,150,000 | 5,000
-- om    |  420 | 2,100,000 | 5,000

الأخطاء الشائعة

الخطأ السبب الحل
Webhook 403 signature خاطئة raw body + HMAC SHA256
Idempotency duplicate idempotency-key مفقود order-id كـ key
session expired 30 دقيقة default تجديد عند العودة من Wave
refund failed session غير محدث تحقق session_id في DB
Cash mismatch الساعي ينسى تسجيل تطبيق يفرض إدخال
QR code لا يعمل payment link expired regenerate كل 5 دقائق

التكيف مع السياق

خمس توضيحات. Wave SN/CI/UG. Wave نشط في السنغال وكوت ديفوار وأوغندا. لـ مالي/بوركينا، Orange Money + Free Money. عمولة Wave 1%. أرخص بكثير من Stripe (2.9%) وVisa (3-4%). جذابة للهامش. Settlement فوري. Wave يحول إلى حسابك المصرفي يومياً (D+1). لا يجب الانتظار 7 أيام مثل Stripe. KYC صارم. Wave يتحقق من هوية التاجر والمعاملات. الالتزام بـ AML. Disputes نادرة. Wave يحل النزاعات بسرعة. أقل من 0.1% chargebacks.

دروس الإخوة

الأسئلة المتكررة

Wave vs Orange Money؟ Wave أرخص (1% vs 1.5%) وUX أبسط. OM مهيمن في كوت ديفوار. الاثنان معاً مثاليان.

Wave Cash-out؟ الساعي يستلم Wave، يحول إلى نقد عند Wave agent. عمولة 0.5% للسحب.

Wave Bulk Payment؟ ادفع 100 ساعٍ في وقت واحد عبر API Bulk. أقل تكلفة.

الأمن؟ Wave يستخدم E2E encryption + 2FA. لم يحدث اختراق رئيسي مذكور.

Sandbox للاختبار؟ Wave يقدم sandbox (api-sandbox.wave.com) لاختبار التكاملات قبل الإنتاج.

للاستزادة

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité