Skip to content

在您的网站上集成 SOAMTAR ​

本指南介绍如何分三步在您的网站上接受塞拉利昂 Orange Money 付款:

  1. 服务器端:创建交易(使用您的私钥认证的请求)。
  2. 浏览器端:通过 JS SDK 打开 SOAMTAR 支付窗口。
  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 拥有商户账户(live 模式需通过 KYC)。
  • 您的 API 密钥,位于 开发者 → API 密钥:
    • sk_test_… 用于沙盒
    • sk_live_… 用于生产环境
  • 一个公开的 HTTPS Webhook 端点(测试模式接受 ngrok / cloudflared 隧道)。

切勿在浏览器中暴露 sk_live_

所有 SOAMTAR API 密钥都是机密。JS 窗口从不需要它:它调用您的后端,由后端发起服务器到服务器的请求。

2. 后端:创建交易 ​

您的后端暴露一个内部路由(例如 POST /checkout/soamtar)供浏览器调用。该路由:

  1. 校验用户和购物车。
  2. 调用 POST https://app.so-amtar.com/v1/payments。
  3. 将 payment_url 和 id 返回给浏览器。

SOAMTAR 端点 ​

方法URL
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-* 属性 ​

对于静态网站或 CMS,无需编写自定义 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>

SDK 在加载时扫描 [data-soamtar],以 POST 调用指定的 URL,从 JSON 响应中读取 paymentUrl 并打开窗口。

展示模式 ​

常量行为
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:由您自行展示。

回调 ​

选项签名触发时机
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_…
基础 URLhttps://app.so-amtar.com/v1相同
Orange Money模拟(模拟成功 / 失败 按钮)真实,需要 KYC
资金流动无实际金额
响应中的 livemodefalsetrue

要在沙盒中强制某个状态,请在窗口中打开 payment_url 并点击相应按钮。Webhook 的触发方式与生产环境完全相同。

6. 上线前检查清单 ​

  • [ ] 您的后端在响应浏览器之前保存了 transaction.id 和 merchant_ref。
  • [ ] 窗口从不自行决定订单状态:Webhook 才是可信来源。
  • [ ] 您的 Webhook 端点校验 t、v1 和 5 分钟窗口,并在 5 秒内返回 2xx。
  • [ ] 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(平台默认 2 小时)。过期后,SOAMTAR 会发送 payment.expired。

同一订单可以同时有多笔付款吗? 可以,但应避免。每次尝试使用唯一的 merchant_ref(例如 ORDER-42-v2),以便在对账时保持一一对应。

客户在手机上会看到什么? 一条来自您企业的 Orange Money 请求,金额为准确的利昂数额。客户用 Orange Money PIN 批准即可;无需安装任何其他东西。

SOAMTAR Payment API Documentation.