دمج SOAMTAR في موقعك
يوضّح هذا الدليل كيفية قبول المدفوعات عبر Orange Money سيراليون على موقعك في ثلاث خطوات:
- على الخادم: إنشاء معاملة (طلب مُصادَق بمفتاحك السرّي).
- في المتصفح: فتح نافذة الدفع من SOAMTAR عبر حزمة JS SDK.
- على الخادم: استقبال الـ 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) يستدعيه المتصفح. هذا المسار:
- يتحقق من المستخدم وسلة الشراء.
- يستدعي
POST https://app.so-amtar.com/v1/payments. - يعيد
payment_urlوidإلى المتصفح.
نقطة نهاية SOAMTAR
| الطريقة | الرابط |
|---|---|
POST | https://app.so-amtar.com/v1/payments |
الترويسات:
| الترويسة | القيمة |
|---|---|
Authorization | Bearer sk_test_… أو sk_live_… |
Content-Type | application/json |
الجسم:
| الحقل | النوع | إلزامي | الوصف |
|---|---|---|---|
merchant_ref | string | نعم | مرجعك الفريد (رقم الطلب). |
amount | numeric | نعم | المبلغ بالليون، بخانتين عشريتين كحد أقصى (مثال: 150.00 = Le 150.00). |
currency | string | لا | SLE افتراضيًا. |
provider | string | لا | orange_money_sl. احذفه لاستخدام نافذة الدفع من SOAMTAR (Hosted Checkout). |
payer.phone | string | موصى به | بتنسيق E.164 (+232 متبوعًا بـ 8 أرقام). يملأ نافذة الدفع مسبقًا. |
payer.name | string | لا | الاسم المعروض على الإيصال. |
return_url | string | موصى به | الصفحة التي يصل إليها العميل بعد دفع ناجح (اسم بديل لـ success_url). |
cancel_url | string | موصى به | الصفحة التي يصل إليها العميل بعد فشل أو إلغاء (اسم بديل لـ failed_url). |
metadata | object | لا | حتى 50 مفتاحًا حرًا، تُعاد في الـ webhook. |
أمثلة
// 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');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 })
})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
{
"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 في نافذة منبثقة، وتستمع لأحداث الدفع، وتُعلمك عند انتهاء الدفع.
تضمين السكربت
<script src="https://app.so-amtar.com/sdk/soamtar.js" defer></script>تقدّمه SOAMTAR مع تخزين مؤقت لمدة يوم واحد.
دمج بسيط: نافذة منبثقة
<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 مخصص:
<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
<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
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>).
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 في الاستجابات | false | true |
لفرض حالة في بيئة الاختبار، افتح 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؛ ولا حاجة إلى تثبيت أي شيء آخر.