Skip to content

Node.js SDK ​

@soamtar/sdk is the official Node.js SDK: collect payments and send payouts with Orange Money Sierra Leone, in leones (SLE).

Server side only

The SDK authenticates with your secret key (sk_test_… / sk_live_…) and refuses any other key. Never ship that key to a browser or a mobile app: there, use the browser SDK with your public key.

Installation ​

bash
npm install @soamtar/sdk

Node.js 18 or later, no runtime dependencies. Current version: 1.1.0 (see the SDK's package.json and CHANGELOG). Until the package is published on npm, install it from the SDK repository.

Configuration ​

js
const soamtar = require('@soamtar/sdk')(process.env.SOAMTAR_SECRET_KEY);
// or, with ES modules / TypeScript:
// import soamtarFactory from '@soamtar/sdk'
// const soamtar = soamtarFactory(process.env.SOAMTAR_SECRET_KEY)
Environment variableRole
SOAMTAR_SECRET_KEYSecret key, read when none is passed to the factory.
SOAMTAR_API_URLOverrides the base URL (default https://app.so-amtar.com/v1). SOAMTAR_BASE_URL is still read as a fallback (deprecated name).

Options (second argument): baseUrl, timeout (ms, default 15000), signRequests (see Request signing).

There is no separate sandbox host: an sk_test_ key goes through simulation on the same URL. soamtar.isTestMode tells you which key is loaded.

Create a payment ​

js
const { data: payment } = await soamtar.payments.create({
  merchant_ref: 'ORDER-1042',
  amount: '1250.00',              // SLE, 2 decimals
  phone: '+23276123456',          // +232 followed by 8 digits
  description: 'Order #1042',
  success_url: 'https://shop.example.sl/thanks',
  failed_url: 'https://shop.example.sl/payment-failed',
  callback_url: 'https://shop.example.sl/webhooks/soamtar', // optional
})

// Send the customer to the hosted checkout, then wait for the webhook.
console.log(payment.id, payment.status, payment.payment_url)
  • currency defaults to SLE (USD and EUR are also accepted) and provider to orange_money_sl, the only provider.
  • Amounts are sent as strings with two decimals: toAmount(1250) → "1250.00".
  • The customer approves the payment on their phone with their Orange Money PIN.
  • Notifications are sent to the webhook endpoints you register (dashboard or webhooks.create). The optional callback_url (HTTPS) also receives every event of this payment, with the same payload and the same X-Soamtar-Signature, so verify it exactly like your endpoint (you need at least one registered endpoint: its secret signs the callback). Same for callback_url on payouts.

API ​

Every method returns the parsed JSON response: single objects as { data }, lists as { data: [], meta } (refunds.list() returns { data: [], pagination }).

MethodEndpoint
payments.create(params, { idempotencyKey? })POST /v1/payments
payments.retrieve(id)GET /v1/payments/{id}
payments.list({ per_page, page })GET /v1/payments (per_page 1–100, default 25)
payments.verify(id)POST /v1/payments/{id}/verify (re-reads the status from Orange Money)
payments.status(idOrReference)GET /v1/status/{idOrReference}
payouts.create(params, { idempotencyKey? })POST /v1/payouts
payouts.retrieve(id)GET /v1/payouts/{id}
payouts.list(query)GET /v1/payouts
refunds.create({ transaction_id, amount?, reason?, description? }, { idempotencyKey? })POST /v1/refunds
refunds.retrieve(idOrReference)GET /v1/refunds/{id} (id or REF-… reference)
refunds.list({ status?, transaction_id?, per_page?, page? })GET /v1/refunds
balance.retrieve()GET /v1/balance
webhooks.list()GET /v1/webhooks
webhooks.create({ url, subscribed_events?, description?, api_version? })POST /v1/webhooks (idempotent by URL; subscribed_events defaults to payment.success, payment.failed, payout.success, payout.failed)
webhooks.secret(endpointId)GET /v1/webhooks/{endpoint_id}/secret
webhooks.lookup(url)GET /v1/webhooks/lookup?url=…
webhooks.sendTest()POST /v1/webhooks/test (sends a webhook.test event; webhooks.test() is a deprecated alias)
request(method, path, body?, { query?, idempotencyKey? })any other endpoint

retrieve() and status() take the SOAMTAR id or reference (TXN-…), not your merchant_ref.

Idempotency ​

Every POST carries an Idempotency-Key header. The SDK generates a UUID v4 per call; pass your own key so that a retry never charges twice:

js
await soamtar.payments.create(params, { idempotencyKey: `order-${order.id}` })

Refunds ​

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.

js
const { data: refund } = await soamtar.refunds.create({
  transaction_id: payment.id,     // SOAMTAR id (or reference) of the payment
  amount: '400.00',               // omit for a full refund of what remains refundable
  reason: 'customer_request',
  description: 'Partial refund for order #1042',
}, { idempotencyKey: `refund-${order.id}-1` })

console.log(refund.reference, refund.status) // "REF-…", "processing"

const { data: again } = await soamtar.refunds.retrieve(refund.reference)
const { data: refunds, pagination } = await soamtar.refunds.list({ transaction_id: payment.id })

The refund is submitted in the same request: with a test key it comes back success; with Orange Money it is processing until Orange confirms; failed (with failure_reason) if the credit is refused. The final outcome arrives by webhook: refund.success or refund.failed, plus payment.refunded / payment.partially_refunded on success. See Refunds for the object and error codes.

Errors ​

Failed calls reject with a SoamtarError:

js
const { SoamtarError } = require('@soamtar/sdk')

try {
  await soamtar.payments.create({ merchant_ref: 'ORDER-1', amount: '0' })
} catch (e) {
  if (e instanceof SoamtarError) {
    console.error(e.status, e.code, e.message, e.errors, e.requestId)
    // 422 validation_error "…" { amount: ['…'] } "req_…"
  }
}

code is the API's error.code (validation_error, …), or timeout, connection_error, invalid_response for transport failures. Quote requestId (the X-Request-Id header) when you contact support. Retry 5xx and network errors with the same idempotencyKey; never retry a 4xx as is.

Webhooks ​

SOAMTAR signs each delivery with X-Soamtar-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of "<t>.<raw body>" with the endpoint secret. Webhook.constructEvent() checks it against the raw body and returns the parsed event:

js
const express = require('express')
const { Webhook } = require('@soamtar/sdk')

const app = express()

app.post('/webhooks/soamtar', express.raw({ type: 'application/json' }), (req, res) => {
  let event
  try {
    event = Webhook.constructEvent(
      req.body,
      req.get('X-Soamtar-Signature'),
      process.env.SOAMTAR_WEBHOOK_SECRET,
    )
  } catch (e) {
    return res.status(400).send(e.message)
  }

  if (event.event === 'payment.success') {
    // event.data.id, event.data.reference, event.data.merchant_reference,
    // event.data.amount, event.data.currency …
  }
  res.sendStatus(200)
})
  • Deliveries older than 300 seconds are rejected ({ tolerance } as fourth argument to change it).
  • During a secret rotation the header also carries v2=…. Pass both secrets ([secret, secret_next], as returned by webhooks.secret()) and either signature is accepted.
  • Endpoints created with api_version: '2026-06-01' receive { id, object: 'event', type, created, data: { object } } instead of { event, created, data }.
  • 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. See Webhooks.
  • webhook.test is sent by webhooks.sendTest() and by the dashboard's test button, whatever your subscriptions: answer it with a 2xx.

Raw body

Most Node frameworks parse JSON automatically. Expose the raw body (string or Buffer) on the webhook route, otherwise the signature will never match. With NestJS, create the app with rawBody: true and pass req.rawBody; with Fastify, register a raw-body parser for that route.

Request signing ​

require('@soamtar/sdk')(key, { signRequests: true }) adds Soamtar-Timestamp and Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHOD>.<path>")) to every POST (<path> without the leading slash, e.g. v1/payments; timestamp within ±300 s). It is optional hardening: the Bearer key over HTTPS already authenticates you. See Authentication.

Helpers ​

js
const { toAmount, formatLe } = require('@soamtar/sdk')

toAmount(1250)      // "1250.00"
formatLe('1250')    // "Le 1,250.00"

TypeScript ​

The package ships its own types (index.d.ts): SoamtarClient, CreatePaymentParams, CreatePayoutParams, CreateRefundParams, Refund, WebhookEventType, SoamtarError, Webhook.

ts
import soamtarFactory, { SoamtarError, type CreatePaymentParams } from '@soamtar/sdk'

const soamtar = soamtarFactory(process.env.SOAMTAR_SECRET_KEY)
const params: CreatePaymentParams = { merchant_ref: 'ORDER-1043', amount: '500.00', phone: '+23277123456' }
const { data } = await soamtar.payments.create(params)

Testing your webhook handler ​

ts
import { createHmac } from 'node:crypto'
import { test } from 'node:test'
import assert from 'node:assert'
import { Webhook } from '@soamtar/sdk'

test('accepts a fresh valid signature', () => {
  const secret = 'whsec_test'
  const t = Math.floor(Date.now() / 1000)
  const body = JSON.stringify({ event: 'payment.success', created: new Date().toISOString(), data: {} })
  const sig = `t=${t},v1=${createHmac('sha256', secret).update(`${t}.${body}`).digest('hex')}`

  assert.strictEqual(Webhook.constructEvent(body, sig, secret).event, 'payment.success')
})

SOAMTAR Payment API Documentation.