Skip to content

SDK PHP / Laravel ​

Official PHP client for the SOAMTAR API: collect payments and send payouts with Orange Money Sierra Leone, and verify webhooks.

  • Package: soamtar/soamtar-php (version 1.1.0), namespace Soamtar\.
  • PHP 8.2+ with ext-curl and ext-json. No other runtime dependency.
  • Currency SLE by default (USD and EUR also accepted), amounts as 2-decimal strings ("150.00"), phones in E.164 (+232 + 8 digits), provider orange_money_sl.

Not yet on Packagist

Until the package is published, install it from the SDK repository (Composer vcs or path repository).

Installation ​

bash
composer require soamtar/soamtar-php

Configuration ​

Use your secret key (sk_test_… or sk_live_…) on the server only. Test keys go through the simulation on the same host, no real money moves.

php
use Soamtar\Soamtar;

Soamtar::configure(getenv('SOAMTAR_SECRET_KEY')); // or Soamtar::configure() to read SOAMTAR_SECRET_KEY

Or create a client explicitly (useful for dependency injection):

php
use Soamtar\SoamtarClient;

$soamtar = new SoamtarClient(getenv('SOAMTAR_SECRET_KEY'), [
    'base_url'        => 'https://app.so-amtar.com/v1', // default; also read from SOAMTAR_API_URL, then SOAMTAR_BASE_URL
    'timeout'         => 30,                            // seconds
    'connect_timeout' => 10,
    'sign_requests'   => false,                         // optional request signing, see below
]);

$soamtar->payments->create([...]);
OptionDefaultNotes
base_urlhttps://app.so-amtar.com/v1HTTPS only (plain HTTP allowed on localhost). Falls back to SOAMTAR_API_URL, then SOAMTAR_BASE_URL (deprecated name).
timeout30Total request timeout, in seconds.
connect_timeout10Connection timeout, in seconds.
sign_requestsfalseAdds Soamtar-Timestamp / Soamtar-Signature to every POST (see Request signing).
transportcURLcallable(string $method, string $url, array $headers, ?string $body): array{status: int, body: string}, for tests or a custom HTTP stack.

Every POST carries an Idempotency-Key (UUID v4). Pass your own as the last argument of create() so a retry never charges twice.

Request signing ​

With 'sign_requests' => true, every POST also carries:

Soamtar-Timestamp: <unix seconds>
Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHOD>.<path>"))

<path> is the URL path without the leading slash (for example v1/payments); the timestamp must be within ±300 seconds of the server clock. It is optional hardening: the Bearer key over HTTPS is enough on its own. $client->signatureHeaders('POST', $url) returns the two headers if you build requests yourself. See Authentication.

Collect a payment ​

Direct push to the customer's phone (they confirm with their Orange Money PIN):

php
$payment = Soamtar::payments()->create([
    'merchant_ref' => 'ORDER-1001',
    'amount'       => '150.00',          // Le 150.00
    'provider'     => 'orange_money_sl',
    'phone'        => '+23276123456',
    'description'  => 'Order #1001',
    'callback_url' => 'https://shop.example.sl/soamtar/webhook', // optional
], 'order-1001');                         // Idempotency-Key

$payment['data']['status'];     // "pending" until the customer enters their PIN
$payment['data']['reference'];  // SOAMTAR reference, e.g. TXN-…

Hosted checkout (SOAMTAR collects the phone number on its own page):

php
Soamtar::payments()->create([
    'merchant_ref' => 'ORDER-1002',
    'amount'       => '75.50',
    'success_url'  => 'https://shop.example.sl/thanks',
    'failed_url'   => 'https://shop.example.sl/cart',
])->redirect(); // 302 to data.payment_url

currency defaults to SLE. return_url / cancel_url are still accepted by the API as aliases of success_url / failed_url.

callback_url (optional, HTTPS) is a per-payment webhook URL: SOAMTAR posts every event of this payment to it, with the same payload and X-Soamtar-Signature as to your registered endpoints, so verify it with Webhook::constructEvent() the same way. It is signed with the secret of your oldest active endpoint, so register at least one endpoint. Payouts accept it too.

Check a payment:

php
Soamtar::payments()->retrieve($id);          // SOAMTAR id or reference
Soamtar::payments()->verify($id);            // asks Orange Money for the latest status
Soamtar::payments()->status($idOrReference); // data.id or data.reference (not your merchant_ref)
Soamtar::payments()->list(['status' => 'success', 'per_page' => 20]);

Send a payout ​

php
$payout = Soamtar::payouts()->create([
    'merchant_ref' => 'PAYOUT-042',
    'amount'       => '250.00',
    'recipient'    => ['phone' => '+23276123456', 'name' => 'Aminata Kamara'],
    'description'  => 'Agent commission October',
]);

provider defaults to orange_money_sl and currency to SLE. The key needs the can_payout scope. The final result arrives with the payout.success / payout.failed webhooks.

Refund a payment ​

Refund a successful payment, fully or partially: SOAMTAR credits the amount back to the payer's Orange Money number and debits your balance. The key needs the can_refund scope.

php
$refund = Soamtar::refunds()->create([
    'transaction_id' => $payment['data']['id'], // SOAMTAR id (or reference) of the payment
    'amount'         => '50.00',                // omit for a full refund of what remains
    'reason'         => 'customer_request',
    'description'    => 'Item out of stock',
], 'refund-order-1001');                        // Idempotency-Key

$refund['data']['reference']; // "REF-…"
$refund['data']['status'];    // "success" (test key), "processing" (Orange Money) or "failed"

Soamtar::refunds()->retrieve($refund['data']['reference']); // id or reference
Soamtar::refunds()->list(['transaction_id' => $payment['data']['id'], 'status' => 'success']);

The refund is submitted in the same request; with Orange Money it stays processing until Orange confirms the credit. The outcome arrives with the refund.success / refund.failed webhooks, plus payment.refunded / payment.partially_refunded on success. See Refunds for the object and error codes.

Webhooks ​

Notifications are sent to the webhook endpoints you register (dashboard or webhooks()->create()), signed with that endpoint's secret. Each delivery carries X-Soamtar-Signature: t=<unix>,v1=<hex> (plus v2= while the secret is being rotated). The signature is HMAC-SHA256(secret, "<t>.<raw body>"), accepted for 300 seconds.

php
use Soamtar\Exceptions\SignatureException;
use Soamtar\Webhook;

try {
    $event = Webhook::constructEvent(
        file_get_contents('php://input'),               // RAW body
        $_SERVER['HTTP_X_SOAMTAR_SIGNATURE'] ?? '',
        getenv('SOAMTAR_WEBHOOK_SECRET'),
    );
} catch (SignatureException $e) {
    http_response_code(400);
    exit;
}

match ($event['event']) {
    'payment.success' => markOrderPaid($event['data']['merchant_reference']),
    'payment.failed', 'payment.cancelled', 'payment.expired' => markOrderFailed($event['data']['merchant_reference']),
    'refund.success' => markOrderRefunded($event['data']['merchant_reference'], $event['data']['amount']),
    default => null, // webhook.test and anything else: just answer 2xx
};

http_response_code(200);

Deliveries can be retried: make your handler idempotent on data.id.

Events you can subscribe to: payment.success, payment.failed, payment.cancelled, payment.expired, payment.refunded, payment.partially_refunded, payout.success, payout.failed, payout.cancelled, refund.success, refund.failed, payout_request.submitted, payout_request.approved, payout_request.rejected, payout_request.completed, payout_request.failed. The test event webhook.test (from sendTest() or the dashboard) is sent whatever your subscriptions and only needs a 2xx. See Webhooks.

Without a stored secret (AutoVerifier) ​

If you don't keep SOAMTAR_WEBHOOK_SECRET, AutoVerifier fetches the endpoint secret with your secret key (scope can_manage_webhooks) and caches it:

php
use Soamtar\SoamtarClient;
use Soamtar\Webhook\AutoVerifier;
use Soamtar\Webhook\FilesystemWebhookSecretCache;

$verifier = new AutoVerifier(
    new SoamtarClient(getenv('SOAMTAR_SECRET_KEY')),
    new FilesystemWebhookSecretCache(__DIR__.'/storage/soamtar-cache'),
);

$event = $verifier->constructEvent($payload, $signatureHeader, getenv('SOAMTAR_WEBHOOK_ENDPOINT_ID'));

Manage endpoints ​

php
Soamtar::webhooks()->create(['url' => 'https://shop.example.sl/soamtar/webhook']); // idempotent by URL
Soamtar::webhooks()->list();
Soamtar::webhooks()->lookup('https://shop.example.sl/soamtar/webhook');
Soamtar::webhooks()->secret($endpointId);
Soamtar::webhooks()->sendTest(); // sends webhook.test to every active endpoint

create() subscribes to payment.success, payment.failed, payout.success and payout.failed unless you pass subscribed_events.

Deprecated aliases, still working: webhooks()->all() (use list()), webhooks()->revealSecret() (use secret()), balance()->show() (use retrieve()).

Laravel ​

Bind the client once, in AppServiceProvider::register():

php
use Soamtar\SoamtarClient;

$this->app->singleton(SoamtarClient::class, fn () => new SoamtarClient(config('services.soamtar.secret')));
php
// config/services.php
'soamtar' => [
    'secret'         => env('SOAMTAR_SECRET_KEY'),
    'webhook_secret' => env('SOAMTAR_WEBHOOK_SECRET'),
],

Controller:

php
use Illuminate\Http\Request;
use Soamtar\Exceptions\SignatureException;
use Soamtar\SoamtarClient;
use Soamtar\Webhook;

final class CheckoutController
{
    public function __construct(private readonly SoamtarClient $soamtar) {}

    public function pay(Order $order)
    {
        $payment = $this->soamtar->payments->create([
            'merchant_ref' => $order->reference,
            'amount'       => number_format($order->total, 2, '.', ''),
            'success_url'  => route('orders.thanks', $order),
            'failed_url'   => route('orders.show', $order),
        ], 'order-'.$order->id);

        return redirect()->away($payment['data']['payment_url']);
    }

    public function webhook(Request $request)
    {
        try {
            $event = Webhook::constructEvent(
                $request->getContent(),
                (string) $request->header('X-Soamtar-Signature'),
                config('services.soamtar.webhook_secret'),
            );
        } catch (SignatureException) {
            abort(400);
        }

        if ($event['event'] === 'payment.success') {
            Order::where('reference', $event['data']['merchant_reference'])->first()?->markPaid();
        }

        return response()->noContent();
    }
}

Exclude the webhook route from CSRF in bootstrap/app.php:

php
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['soamtar/webhook']);
})

Errors ​

ExceptionWhen
Soamtar\Exceptions\ApiExceptionHTTP status ≥ 400. getHttpStatus(), getErrorCode(), getRawResponse() (secrets masked), isRetryable() (429 and 5xx).
Soamtar\Exceptions\SignatureExceptionInvalid, expired or malformed webhook signature.
Soamtar\Exceptions\SoamtarExceptionBase class: configuration, network or non-JSON response.
php
use Soamtar\Exceptions\ApiException;

try {
    Soamtar::payouts()->create($params, $idempotencyKey);
} catch (ApiException $e) {
    if ($e->isRetryable()) {
        // retry later with the SAME idempotency key
    }
    error_log($e->getErrorCode().': '.$e->getMessage());
}

Reference ​

FacadeMethods
Soamtar::payments()create(array $params, string $idempotencyKey = ''), retrieve($id), list(array $params = []), status($idOrReference), verify($id), listSplits($transactionId), createSplit($transactionId, array $params, string $idempotencyKey = '')
Soamtar::payouts()create(array $params, string $idempotencyKey = ''), retrieve($id), list(array $params = [])
Soamtar::refunds()create(array $params, string $idempotencyKey = ''), retrieve($idOrReference), list(array $params = [])
Soamtar::payoutRequests()create(array $params, string $idempotencyKey = ''), list(array $params = []), retrieve($id)
Soamtar::subscriptions()create(['plan_id' => …, 'customer_id' => …]), retrieve($id), list(array $params = []), cancel($id)
Soamtar::webhooks()list(), create(array $payload), lookup(string $url), secret($endpointId), sendTest()
Soamtar::balance()retrieve()
Soamtar::providers()all()
Soamtar::status()retrieve(string $reference) (same as payments()->status())
Soamtar::disputes()list(array $params = []), retrieve($id), respond($id, array $params, string $idempotencyKey = '')
Soamtar::settlements()list(array $params = []), retrieve($id)
Soamtar::health()GET /v1/health

The same resources are available as properties on a SoamtarClient instance ($soamtar->payments, $soamtar->payouts, …). Every method returns a Soamtar\SoamtarResponse (array access, get(), toArray(), successful(), redirect()).

Testing your webhook handler ​

Webhook::sign() builds a valid header, handy in Pest or PHPUnit:

php
use Soamtar\Webhook;

it('accepts a signed payment.success', function () {
    config()->set('services.soamtar.webhook_secret', 'whsec_test');
    $payload = json_encode(['event' => 'payment.success', 'data' => ['id' => 'tx_1', 'merchant_reference' => 'ORDER-1001']]);

    $this->call('POST', '/soamtar/webhook', [], [], [], [
        'HTTP_X_SOAMTAR_SIGNATURE' => Webhook::sign($payload, 'whsec_test'),
        'CONTENT_TYPE'             => 'application/json',
    ], $payload)->assertNoContent();
});

SOAMTAR Payment API Documentation.