Ruby SDK
The official SOAMTAR gem (soamtar 1.1.0): collect payments and send payouts with Orange Money Sierra Leone, in leones (SLE).
- Ruby 2.6+, no runtime dependencies (
net/http,openssl,json) - Sierra Leone defaults: currency
SLE, providerorange_money_sl - Amounts as 2-decimal strings (
"150.00"),IntegerorBigDecimal, neverFloat - An
Idempotency-Keyis sent automatically on every POST - Webhook signature verification (
X-Soamtar-Signature)
Server-side only
The client takes a secret key (sk_test_… or sk_live_…). Never ship it to a browser or a mobile app.
Installation
# Gemfile
gem 'soamtar'Until the gem is published, install it from the repository (gem 'soamtar', git: '<repository URL>').
Configuration
require 'soamtar'
client = Soamtar::Client.new # reads SOAMTAR_SECRET_KEY
client = Soamtar::Client.new('sk_test_...') # or pass the secret key
# Shared client, e.g. config/initializers/soamtar.rb
Soamtar.configure(ENV['SOAMTAR_SECRET_KEY'])
Soamtar.payments.create(...)| Option | Default | Description |
|---|---|---|
base_url: | SOAMTAR_API_URL, then SOAMTAR_BASE_URL (deprecated name), else https://app.so-amtar.com/v1 | API root |
timeout: | 15 | Timeout in seconds |
sign_requests: | false | Signs every POST (see Request signing) |
client = Soamtar::Client.new(ENV['SOAMTAR_SECRET_KEY'], timeout: 30)
client.test_mode? # true with a sk_test_ keysk_test_ keys run on the same host as live keys: payments are simulated and no money moves.
Request signing
Soamtar::Client.new(key, sign_requests: true) adds Soamtar-Timestamp: <unix> and Soamtar-Signature: v1=hex(HMAC_SHA256(secret_key, "<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.
Collect a payment
payment = client.payments.create(
merchant_ref: 'ORDER-1042',
amount: '150.00', # Le 150.00
phone: '+23276123456', # +232 followed by 8 digits
description: 'Order #1042',
success_url: 'https://shop.example.sl/thanks',
failed_url: 'https://shop.example.sl/cart',
callback_url: 'https://shop.example.sl/webhooks/soamtar' # optional
) # currency 'SLE' and provider 'orange_money_sl' by default
payment['data']['status'] # "pending"
payment.payment_url # hosted checkout page (data.payment_url)callback_url (optional, HTTPS, payments and payouts) is a per-payment webhook URL: SOAMTAR sends it every event of that transaction with the same payload and X-Soamtar-Signature as your registered endpoints (signed with the secret of your oldest active endpoint, so register at least one).
In Rails, send the customer to the hosted page:
redirect_to payment.payment_url, allow_other_host: truePass your own Idempotency-Key to make retries safe:
client.payments.create(params, idempotency_key: 'order-1042-attempt-1')Other calls:
client.payments.retrieve(id) # by SOAMTAR id or reference
client.payments.retrieve(id, wait: true) # long poll until a final status
client.payments.verify(id) # re-check the status with Orange Money
client.payments.list(status: 'success', per_page: 50)
client.payments.status('TXN-1A2B3C4D-...') # by SOAMTAR id or reference
client.balance.retrieveSend a payout
client.payouts.create(
merchant_ref: 'PAYOUT-77',
amount: '50.00',
recipient: { phone: '+23276123456', name: 'Aminata Kamara' },
description: 'Supplier payment'
)
client.payouts.retrieve(id)
client.payouts.list(status: 'success')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.
refund = client.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' },
idempotency_key: 'refund-order-1042-1'
)
refund['data']['reference'] # "REF-…"
refund['data']['status'] # "success" (test key), "processing" (Orange Money) or "failed"
client.refunds.retrieve(refund['data']['reference']) # id or reference
client.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 refund.success / refund.failed, plus payment.refunded / payment.partially_refunded on success. See Refunds for the object and error codes.
Webhooks
SOAMTAR posts events to the endpoints you register (dashboard or webhooks.create), signed with that endpoint's secret: X-Soamtar-Signature: t=<unix>,v1=<hmac> (HMAC-SHA256 of "<t>.<raw body>"). During a secret rotation a v2= signature is added; either one is accepted. Signatures older than 300 seconds are rejected.
Rails
class SoamtarWebhooksController < ActionController::API
def create
event = Soamtar::Webhook.construct_event(
request.raw_post, # RAW body
request.headers['X-Soamtar-Signature'],
ENV.fetch('SOAMTAR_WEBHOOK_SECRET')
)
case event['event']
when 'payment.success'
Order.mark_paid!(event['data']['merchant_reference'])
when 'payment.failed', 'payment.expired', 'payment.cancelled'
Order.mark_failed!(event['data']['merchant_reference'])
when 'refund.success'
Order.mark_refunded!(event['data']['merchant_reference'], event['data']['amount'])
end # webhook.test and others: just 200
head :ok
rescue Soamtar::SignatureError
head :bad_request
end
endSinatra
require 'sinatra'
require 'soamtar'
post '/webhooks/soamtar' do
event = Soamtar::Webhook.construct_event(
request.body.read,
request.env['HTTP_X_SOAMTAR_SIGNATURE'],
ENV.fetch('SOAMTAR_WEBHOOK_SECRET')
)
# handle event['event'] / event['data']
status 200
rescue Soamtar::SignatureError
status 400
endThe same event can be delivered more than once: deduplicate on event + 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. webhook.test is sent by client.webhooks.send_test and the dashboard's test button whatever your subscriptions. See Webhooks.
Manage endpoints
client.webhooks.create('https://shop.example.sl/webhooks/soamtar') # defaults: payment.success/failed, payout.success/failed
client.webhooks.create('https://shop.example.sl/webhooks/soamtar', %w[payment.success refund.success refund.failed])
client.webhooks.list
client.webhooks.lookup('https://shop.example.sl/webhooks/soamtar')
client.webhooks.secret(endpoint_id) # signing secret
client.webhooks.send_test # sends a webhook.test eventDeprecated aliases, still working: client.webhooks.test (use send_test), client.status.retrieve (use client.payments.status).
Errors
begin
client.payments.create(...)
rescue Soamtar::ApiError => e
e.status # HTTP status
e.code # API error code, e.g. "validation_error"
e.errors # field messages on 422
rescue Soamtar::Error => e
e.message # configuration, network or invalid input
endSecurity
- Constant-time signature comparison.
- The secret key is masked in
inspect,to_sandmarshal_dump. - Webhook payloads over 1 MB are rejected.