SDK Node.js
@soamtar/sdk est le SDK Node.js officiel : il encaisse des paiements et envoie des payouts via Orange Money Sierra Leone, en leones (SLE).
Côté serveur uniquement
Le SDK s'authentifie avec votre clé secrète (sk_test_… / sk_live_…) et refuse toute autre clé. Cette clé ne doit jamais se retrouver dans un navigateur ou une application mobile : utilisez-y le SDK navigateur avec votre clé publique.
Installation
npm install @soamtar/sdkNode.js 18 ou plus récent, aucune dépendance à l'exécution. Version actuelle : 1.1.0 (voir le package.json et le CHANGELOG du SDK). Tant que le paquet n'est pas publié sur npm, installez-le depuis le dépôt du SDK.
Configuration
const soamtar = require('@soamtar/sdk')(process.env.SOAMTAR_SECRET_KEY);
// ou, en modules ES / TypeScript :
// import soamtarFactory from '@soamtar/sdk'
// const soamtar = soamtarFactory(process.env.SOAMTAR_SECRET_KEY)| Variable d'environnement | Rôle |
|---|---|
SOAMTAR_SECRET_KEY | Clé secrète, lue si aucune clé n'est passée à la fabrique. |
SOAMTAR_API_URL | Remplace l'URL de base (par défaut https://app.so-amtar.com/v1). SOAMTAR_BASE_URL reste lu en repli (ancien nom, déprécié). |
Options (second argument) : baseUrl, timeout (en ms, 15000 par défaut), signRequests (voir Signature des requêtes).
Il n'existe pas d'hôte sandbox distinct : une clé sk_test_ passe par la simulation sur la même URL. soamtar.isTestMode indique quel type de clé est chargé.
Créer un paiement
const { data: payment } = await soamtar.payments.create({
merchant_ref: 'ORDER-1042',
amount: '1250.00', // en SLE, 2 décimales
phone: '+23276000001', // +232 suivi de 8 chiffres
description: 'Commande #1042',
success_url: 'https://shop.example.sl/merci',
failed_url: 'https://shop.example.sl/paiement-echoue',
callback_url: 'https://shop.example.sl/webhooks/soamtar', // facultatif
})
// Redirigez le client vers le checkout hébergé, puis attendez le webhook.
console.log(payment.id, payment.status, payment.payment_url)currencyvautSLEpar défaut (USDetEURsont aussi acceptés) etprovidervautorange_money_sl, le seul opérateur.- Les montants partent sous forme de chaîne à deux décimales :
toAmount(1250)→"1250.00". - Le client valide le paiement sur son téléphone avec son code PIN Orange Money.
- Les notifications sont envoyées aux endpoints webhook que vous avez enregistrés (dashboard ou
webhooks.create). Lecallback_urlfacultatif (HTTPS) reçoit en plus tous les événements de ce paiement, avec le même payload et la mêmeX-Soamtar-Signature: vérifiez-le exactement comme votre endpoint (il vous faut au moins un endpoint enregistré, dont le secret signe le callback). Idem pourcallback_urlsur les payouts.
API
Chaque méthode renvoie la réponse JSON décodée : { data } pour un objet, { data: [], meta } pour une liste (refunds.list() renvoie { data: [], pagination }).
| Méthode | Endpoint |
|---|---|
payments.create(params, { idempotencyKey? }) | POST/v1/payments |
payments.retrieve(id) | GET/v1/payments/{id} |
payments.list({ per_page, page }) | GET/v1/payments (per_page de 1 à 100, 25 par défaut) |
payments.verify(id) | POST/v1/payments/{id}/verify (relit le statut auprès d'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} (identifiant ou référence REF-…) |
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 par URL ; subscribed_events vaut par défaut 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 (envoie un événement webhook.test ; webhooks.test() reste un alias déprécié) |
request(method, path, body?, { query?, idempotencyKey? }) | tout autre endpoint |
retrieve() et status() attendent l'identifiant ou la référence SOAMTAR (TXN-…), pas votre merchant_ref.
Idempotence
Chaque POST porte un en-tête Idempotency-Key. Le SDK génère un UUID v4 à chaque appel ; fournissez votre propre clé pour qu'une nouvelle tentative ne débite jamais deux fois :
await soamtar.payments.create(params, { idempotencyKey: `order-${order.id}` })Remboursements
Remboursez un paiement réussi, en totalité ou en partie : SOAMTAR recrédite le montant sur le numéro Orange Money du payeur et débite votre solde. La clé doit avoir le scope can_refund.
const { data: refund } = await soamtar.refunds.create({
transaction_id: payment.id, // identifiant (ou référence) SOAMTAR du paiement
amount: '400.00', // omis : remboursement total du restant remboursable
reason: 'customer_request',
description: 'Remboursement partiel de la commande #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 })Le remboursement est soumis dans la même requête : avec une clé de test il revient success ; avec Orange Money il reste processing jusqu'à la confirmation d'Orange ; failed (avec failure_reason) si le crédit est refusé. Le résultat final arrive par webhook : refund.success ou refund.failed, plus payment.refunded / payment.partially_refunded en cas de succès. Voir Remboursements pour l'objet et les codes d'erreur.
Erreurs
Un appel en échec est rejeté avec une SoamtarError :
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 reprend le error.code de l'API (validation_error, …), ou vaut timeout, connection_error, invalid_response pour les erreurs de transport. Communiquez le requestId (en-tête X-Request-Id) au support. Relancez les erreurs 5xx et réseau avec la même idempotencyKey ; ne relancez jamais une 4xx telle quelle.
Webhooks
SOAMTAR signe chaque envoi avec X-Soamtar-Signature: t=<unix>,v1=<hex>, un HMAC-SHA256 de "<t>.<corps brut>" calculé avec le secret de l'endpoint. Webhook.constructEvent() le vérifie sur le corps brut et renvoie l'événement décodé :
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)
})- Les envois de plus de 300 secondes sont rejetés (quatrième argument
{ tolerance }pour changer ce délai). - Pendant une rotation du secret, l'en-tête contient aussi
v2=…. Passez les deux secrets ([secret, secret_next], tels que renvoyés parwebhooks.secret()) : l'une ou l'autre signature est acceptée. - Les endpoints créés avec
api_version: '2026-06-01'reçoivent{ id, object: 'event', type, created, data: { object } }au lieu de{ event, created, data }. - Événements auxquels s'abonner :
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. Voir Webhooks. webhook.testest envoyé parwebhooks.sendTest()et par le bouton de test du dashboard, quels que soient vos abonnements : répondez-y par un 2xx.
Corps brut
La plupart des frameworks Node décodent le JSON automatiquement. Exposez le corps brut (chaîne ou Buffer) sur la route du webhook, sinon la signature ne correspondra jamais. Avec NestJS, créez l'application avec rawBody: true et passez req.rawBody ; avec Fastify, déclarez un parseur de corps brut pour cette route.
Signature des requêtes
require('@soamtar/sdk')(key, { signRequests: true }) ajoute Soamtar-Timestamp et Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHODE>.<chemin>")) à chaque POST (<chemin> sans la barre oblique initiale, par exemple v1/payments ; horodatage à ±300 s). C'est un durcissement facultatif : la clé Bearer sur HTTPS vous authentifie déjà. Voir Authentification.
Utilitaires
const { toAmount, formatLe } = require('@soamtar/sdk')
toAmount(1250) // "1250.00"
formatLe('1250') // "Le 1,250.00"TypeScript
Le paquet embarque ses propres types (index.d.ts) : SoamtarClient, CreatePaymentParams, CreatePayoutParams, CreateRefundParams, Refund, WebhookEventType, SoamtarError, Webhook.
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: '+23276000002' }
const { data } = await soamtar.payments.create(params)Tester votre gestionnaire de webhook
import { createHmac } from 'node:crypto'
import { test } from 'node:test'
import assert from 'node:assert'
import { Webhook } from '@soamtar/sdk'
test('accepte une signature récente et valide', () => {
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')
})