Skip to content

دمج SOAMTAR في موقعك ​

يوضّح هذا الدليل كيفية قبول المدفوعات عبر Orange Money سيراليون على موقعك في ثلاث خطوات:

  1. على الخادم: إنشاء معاملة (طلب مُصادَق بمفتاحك السرّي).
  2. في المتصفح: فتح نافذة الدفع من SOAMTAR عبر حزمة JS SDK.
  3. على الخادم: استقبال الـ webhook النهائي والتحقق منه.

البنية الموصى بها: لا تتواصل النافذة المنبثقة (JS) مع SOAMTAR باستخدام مفتاحك السرّي أبدًا. بل تستدعي مسارًا في خادمك الخلفي، وهو الذي يستدعي SOAMTAR. هذه هي الطريقة الوحيدة لإبقاء sk_live_… خارج المتصفح.

نظرة عامة على المسار ​

تبقى تسميات المخطط أدناه بالإنجليزية للحفاظ على المحاذاة واتجاه القراءة من اليسار إلى اليمين.

Browser                 Your backend                SOAMTAR                 Customer's phone
   |  click "Pay"            |                          |                          |
   |------------------------>|  POST /v1/payments       |                          |
   |                         |------------------------->|                          |
   |                         |<-------------------------|  payment_url, id         |
   |<------------------------|  { paymentUrl }          |                          |
   |  SOAMTAR window opens   |                          |  Orange Money request    |
   |------------------------------------------------->  |------------------------->|
   |                         |                          |<-------------------------|
   |                         |                          |   customer enters PIN    |
   |                         |<-------------------------|                          |
   |                         |  signed webhook          |                          |
   |                         |  payment.success         |                          |
   |<------------------------|  return_url / JS callback|                          |

1. المتطلبات المسبقة ​

  • حساب تاجر على https://app.so-amtar.com (مع اعتماد KYC لوضع live).
  • مفاتيح API الخاصة بك، من المطوّرون ← مفاتيح API:
    • sk_test_… لبيئة الاختبار
    • sk_live_… للإنتاج
  • نقطة استقبال HTTPS عامة للـ webhooks (يقبل وضع الاختبار أنفاق ngrok / cloudflared).

لا تكشف sk_live_ في المتصفح أبدًا

كل مفاتيح API الخاصة بـ SOAMTAR سرّية. لا تحتاج نافذة JS إليها أبدًا: فهي تستدعي خادمك الخلفي الذي يُجري الطلب من خادم إلى خادم.

2. الخادم الخلفي: إنشاء معاملة ​

يوفّر خادمك الخلفي مسارًا داخليًا واحدًا (مثل POST /checkout/soamtar) يستدعيه المتصفح. هذا المسار:

  1. يتحقق من المستخدم وسلة الشراء.
  2. يستدعي POST https://app.so-amtar.com/v1/payments.
  3. يعيد payment_url وid إلى المتصفح.

نقطة نهاية SOAMTAR ​

الطريقةالرابط
POSThttps://app.so-amtar.com/v1/payments

الترويسات:

الترويسةالقيمة
AuthorizationBearer sk_test_… أو sk_live_…
Content-Typeapplication/json

الجسم:

الحقلالنوعإلزاميالوصف
merchant_refstringنعممرجعك الفريد (رقم الطلب).
amountnumericنعمالمبلغ بالليون، بخانتين عشريتين كحد أقصى (مثال: 150.00 = Le 150.00).
currencystringلاSLE افتراضيًا.
providerstringلاorange_money_sl. احذفه لاستخدام نافذة الدفع من SOAMTAR (Hosted Checkout).
payer.phonestringموصى بهبتنسيق E.164 (+232 متبوعًا بـ 8 أرقام). يملأ نافذة الدفع مسبقًا.
payer.namestringلاالاسم المعروض على الإيصال.
return_urlstringموصى بهالصفحة التي يصل إليها العميل بعد دفع ناجح (اسم بديل لـ success_url).
cancel_urlstringموصى بهالصفحة التي يصل إليها العميل بعد فشل أو إلغاء (اسم بديل لـ failed_url).
metadataobjectلاحتى 50 مفتاحًا حرًا، تُعاد في الـ webhook.

أمثلة ​

php
// routes/web.php or routes/api.php
Route::post('/checkout/soamtar', function (Request $request) {
    $order = Order::findOrFail($request->integer('order_id'));

    $response = Http::withToken(config('services.soamtar.key'))
        ->acceptJson()
        ->post('https://app.so-amtar.com/v1/payments', [
            'merchant_ref' => $order->reference,
            'amount'       => $order->total_sle,
            'currency'     => 'SLE',
            'payer'        => [
                'phone' => $order->customer_phone,
                'name'  => $order->customer_name,
            ],
            'return_url'   => route('checkout.success', $order),
            'cancel_url'   => route('checkout.cancel', $order),
            'metadata'     => [
                'order_id' => $order->id,
                'user_id'  => $request->user()->id,
            ],
        ])
        ->throw()
        ->json('data');

    return response()->json([
        'id'         => $response['id'],
        'paymentUrl' => $response['payment_url'],
    ]);
})->middleware('auth');
ts
import express from 'express'

const app = express()
app.use(express.json())

app.post('/checkout/soamtar', async (req, res) => {
  const order = await db.orders.findById(req.body.order_id)

  const r = await fetch('https://app.so-amtar.com/v1/payments', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SOAMTAR_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      merchant_ref: order.reference,
      amount: order.total_sle,
      currency: 'SLE',
      payer: { phone: order.customerPhone, name: order.customerName },
      return_url: `${process.env.APP_URL}/checkout/success?id=${order.id}`,
      cancel_url: `${process.env.APP_URL}/checkout/cancel?id=${order.id}`,
      metadata: { order_id: order.id, user_id: req.user.id },
    }),
  })

  if (!r.ok) return res.status(502).json({ error: await r.text() })
  const { data } = await r.json()
  res.json({ id: data.id, paymentUrl: data.payment_url })
})
python
import os, httpx
from fastapi import APIRouter, Depends, HTTPException

router = APIRouter()

@router.post("/checkout/soamtar")
async def checkout(order_id: int, user = Depends(auth)):
    order = await db.orders.get(order_id)

    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.post(
            "https://app.so-amtar.com/v1/payments",
            headers={
                "Authorization": f"Bearer {os.environ['SOAMTAR_SECRET_KEY']}",
            },
            json={
                "merchant_ref": order.reference,
                "amount": order.total_sle,
                "currency": "SLE",
                "payer": {"phone": order.customer_phone, "name": order.customer_name},
                "return_url": f"{APP_URL}/checkout/success?id={order.id}",
                "cancel_url": f"{APP_URL}/checkout/cancel?id={order.id}",
                "metadata": {"order_id": order.id, "user_id": user.id},
            },
        )
    if r.status_code >= 400:
        raise HTTPException(502, r.text)
    data = r.json()["data"]
    return {"id": data["id"], "paymentUrl": data["payment_url"]}

استجابة SOAMTAR ​

json
{
  "data": {
    "id": "0e85b3c2-4a01-4e8f-b8b8-b71e0e1c8d1a",
    "object": "transaction",
    "reference": "TXN-A1B2C3D4E5",
    "status": "pending",
    "amount": "150.00",
    "currency": "SLE",
    "payment_url": "https://app.so-amtar.com/pay/checkout/0e85b3c2-...",
    "expires_at": "2026-04-15T12:30:00+00:00"
  },
  "livemode": false
}

يجب على خادمك الخلفي حفظ data.id وdata.reference وربطهما بـ order_id الخاص بك قبل الرد على المتصفح. هذا الجدول هو ما تستخدمه لاحقًا لمطابقة الـ webhook.

3. الواجهة الأمامية: نافذة الدفع من SOAMTAR ​

حزمة JS SDK سكربت بلا تبعيات. تفتح payment_url في نافذة منبثقة، وتستمع لأحداث الدفع، وتُعلمك عند انتهاء الدفع.

تضمين السكربت ​

html
<script src="https://app.so-amtar.com/sdk/soamtar.js" defer></script>

تقدّمه SOAMTAR مع تخزين مؤقت لمدة يوم واحد.

دمج بسيط: نافذة منبثقة ​

html
<button id="pay-btn" data-order-id="42">
  Pay Le 150.00
</button>

<script>
document.getElementById('pay-btn').addEventListener('click', async (e) => {
  const orderId = e.currentTarget.dataset.orderId;

  // 1) Ask YOUR backend to create the SOAMTAR transaction.
  const res = await fetch('/checkout/soamtar', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ order_id: orderId }),
  });
  const { id, paymentUrl } = await res.json();

  // 2) Open the window on the URL returned by SOAMTAR.
  new SOAMTAR({ transactionId: id, paymentUrl })
    .withOption({
      presentationMode: SOAMTAR.OPEN_IN_POPUP,
      locale: 'en',
      didReceivePayment: (event) => {
        // event.status === 'success' | 'failed' | 'cancelled'
        if (event.status === 'success') {
          window.location.href = '/checkout/success?id=' + orderId;
        }
      },
      didCancel: () => console.info('Payment cancelled by the customer'),
      didReceiveError: (err) => console.error('[SOAMTAR]', err),
    })
    .send();
});
</script>

دمج تصريحي: سمات data-* ​

لموقع ثابت أو نظام إدارة محتوى، لا تحتاج إلى JavaScript مخصص:

html
<button
  data-soamtar
  data-soamtar-request-url="/checkout/soamtar"
  data-soamtar-body='{"order_id": 42}'
  data-soamtar-success-url="/checkout/success"
  data-soamtar-mode="popup">
  Pay
</button>

تفحص الحزمة [data-soamtar] عند التحميل، وتستدعي الرابط المحدد بـ POST، وتقرأ paymentUrl من استجابة JSON، ثم تفتح النافذة.

أوضاع العرض ​

الثابتالسلوك
SOAMTAR.OPEN_IN_POPUPنافذة منبثقة في وسط الصفحة (افتراضي).
SOAMTAR.OPEN_IN_NEW_TABيفتح علامة تبويب جديدة (بديل عند حظر النوافذ المنبثقة).
SOAMTAR.OPEN_IN_SAME_TABإعادة توجيه الصفحة بالكامل إلى payment_url.
SOAMTAR.EMBEDمضمّنة في <div id="soamtar-frame"></div> توفّره أنت.
SOAMTAR.HEADLESSلا يفتح شيئًا ويعيد paymentUrl: أنت تعرضه بنفسك.

دوال الاستدعاء (Callbacks) ​

الخيارالتوقيعمتى
didReceivePayment({ status, transactionId, reference })تلقّت النافذة حدثًا نهائيًا.
didCancel()أغلق العميل النافذة أو نقر على «إلغاء».
didReceiveError(error)خطأ شبكة، رمز منتهٍ، CSP…
didClose()يُستدعى دائمًا عند إغلاق النافذة (بعد الدوال الأخرى).

الحدث ≠ إثبات الدفع

دوال الاستدعاء في JS إعلامية فقط (قد يغلق العميل علامة التبويب مبكرًا، أو يفقد التغطية، إلخ). لا تعلّم الطلب أبدًا بأنه مدفوع انطلاقًا من المتصفح: وحده الـ webhook هو المرجع.

Vue.js / Inertia ​

vue
<script setup lang="ts">
import { ref } from 'vue'

const loading = ref(false)

async function pay(orderId: number) {
  loading.value = true
  const { id, paymentUrl } = await fetch('/checkout/soamtar', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf() },
    body: JSON.stringify({ order_id: orderId }),
  }).then(r => r.json())

  new window.SOAMTAR({ transactionId: id, paymentUrl })
    .withOption({
      presentationMode: window.SOAMTAR.OPEN_IN_POPUP,
      didReceivePayment: () => router.visit(`/orders/${orderId}`),
      didClose: () => (loading.value = false),
    })
    .send()
}
</script>

<template>
  <button :disabled="loading" @click="pay(42)">Pay</button>
</template>

React ​

tsx
import { useState } from 'react'

declare global { interface Window { SOAMTAR: any } }

export function PayButton({ orderId }: { orderId: number }) {
  const [loading, setLoading] = useState(false)

  async function pay() {
    setLoading(true)
    const { id, paymentUrl } = await fetch('/checkout/soamtar', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ order_id: orderId }),
    }).then(r => r.json())

    new window.SOAMTAR({ transactionId: id, paymentUrl })
      .withOption({
        presentationMode: window.SOAMTAR.OPEN_IN_POPUP,
        didReceivePayment: e => e.status === 'success' && (location.href = '/success'),
        didClose: () => setLoading(false),
      })
      .send()
  }

  return <button disabled={loading} onClick={pay}>Pay</button>
}

4. الـ webhook: المصدر الوحيد للحقيقة ​

ترسل SOAMTAR طلب POST إلى نقطة الاستقبال لديك بمجرد تغيّر الحالة. يُوقَّع الطلب في الترويسة X-Soamtar-Signature (t=<timestamp>,v1=<hmac>).

php
Route::post('/webhooks/soamtar', function (Request $request) {
    $header = $request->header('X-Soamtar-Signature', '');
    $body   = $request->getContent();
    $secret = config('services.soamtar.webhook_secret');

    if (! preg_match('/^t=(\d+),v1=([a-f0-9]{64})/', $header, $m)) {
        abort(400, 'bad signature');
    }
    if (abs(time() - (int) $m[1]) > 300) {
        abort(401, 'expired');
    }
    $expected = hash_hmac('sha256', $m[1] . '.' . $body, $secret);
    if (! hash_equals($expected, $m[2])) {
        abort(401, 'invalid');
    }

    $event = json_decode($body, true);
    match ($event['event']) {
        'payment.success'   => Order::markPaid($event['data']['merchant_reference']),
        'payment.failed'    => Order::markFailed($event['data']['merchant_reference']),
        'payment.cancelled' => Order::markCancelled($event['data']['merchant_reference']),
        default             => null,
    };

    return response()->json(['received' => true]);
});

التكرار الآمن

يُرسَل كل حدث مرة واحدة على الأقل. استخدم الزوج event + data.id مفتاحًا فريدًا في قاعدة بياناتك: إذا كنت قد عالجته من قبل، فرُدّ بـ 200 دون أي إجراء.

التفاصيل الكاملة، وتدوير السرّ، والتنسيقات ذات الإصدارات: راجع Webhooks.

5. بيئة الاختبار مقابل الإنتاج ​

بيئة الاختبارالإنتاج
المفتاحsk_test_…sk_live_…
الرابط الأساسيhttps://app.so-amtar.com/v1مطابق
Orange Moneyمحاكاة (أزرار محاكاة النجاح / الفشل)حقيقي، KYC إلزامي
الأموال المنقولةلا شيءالمبلغ الفعلي
livemode في الاستجاباتfalsetrue

لفرض حالة في بيئة الاختبار، افتح payment_url في النافذة وانقر على الزر المقابل. يُرسَل الـ webhook تمامًا كما في الإنتاج.

6. قائمة التحقق قبل الإطلاق ​

  • [ ] يحفظ خادمك الخلفي transaction.id وmerchant_ref قبل الرد على المتصفح.
  • [ ] النافذة لا تقرر أبدًا حالة الطلب بمفردها: الـ webhook هو المصدر الوحيد للحقيقة.
  • [ ] تتحقق نقطة استقبال الـ webhook من t وv1 ومن نافذة الدقائق الخمس، وترد بـ 2xx في أقل من 5 ثوانٍ.
  • [ ] يعرض return_url وcancel_url الحالة الحقيقية (من قاعدة بياناتك التي يغذّيها الـ webhook)، لا تأكيدًا متفائلًا.
  • [ ] لا تخزّن السجلات مفتاح API بنص واضح أبدًا.
  • [ ] تسمح سياسة CSP لديك بـ https://app.so-amtar.com (script-src وframe-src وconnect-src) إذا كنت تستخدم النافذة.
  • [ ] اختبار كامل في بيئة الاختبار: النجاح، الفشل، الإلغاء، انتهاء الصلاحية، تكرار الـ webhook.

راجع أيضًا قائمة التحقق المفصّلة للدمج ومن الاختبار إلى الإنتاج (بالإنجليزية).

7. أسئلة شائعة ​

هل النافذة المنبثقة إلزامية؟ لا. يمكنك إعادة التوجيه مباشرة إلى payment_url (window.location.href = paymentUrl). توفّر النافذة تجربة أفضل لأن العميل يبقى على صفحتك.

هل يمكنني استدعاء /v1/payments مباشرة من المتصفح؟ لا، أبدًا. فذلك يكشف مفتاح API الخاص بك. المسار الداخلي /checkout/soamtar هو الطريقة الآمنة الوحيدة.

ما مدة صلاحية payment_url؟ حتى expires_at الذي يُعاد عند الإنشاء (ساعتان افتراضيًا على المنصة). بعد ذلك، ترسل SOAMTAR الحدث payment.expired.

هل يمكن أن تكون هناك عدة مدفوعات متزامنة للطلب نفسه؟ نعم، لكن تجنّب ذلك. استخدم merchant_ref فريدًا لكل محاولة (مثل ORDER-42-v2) للحفاظ على علاقة 1:1 عند المطابقة.

ماذا يرى العميل على هاتفه؟ طلبًا من Orange Money صادرًا عن شركتك بالمبلغ الدقيق بالليون. يوافق عليه برمز PIN الخاص بـ Orange Money؛ ولا حاجة إلى تثبيت أي شيء آخر.

SOAMTAR Payment API Documentation.