Java SDK
The official SOAMTAR library for Java (com.soamtar:soamtar-java 1.1.0): collect payments and send payouts with Orange Money Sierra Leone, in leones (SLE).
- Java 11+, one dependency (Jackson)
- Sierra Leone defaults: currency
SLE, providerorange_money_sl - Amounts as
String("150.00") orBigDecimal, neverdouble/float - 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
Maven:
<dependency>
<groupId>com.soamtar</groupId>
<artifactId>soamtar-java</artifactId>
<version>1.1.0</version>
</dependency>Gradle:
implementation 'com.soamtar:soamtar-java:1.1.0'Until the artifact is published, build it from the repository (mvn install).
Configuration
import com.soamtar.sdk.SoamtarClient;
SoamtarClient soamtar = new SoamtarClient(); // reads SOAMTAR_SECRET_KEY and SOAMTAR_API_URL
SoamtarClient explicit = new SoamtarClient(System.getenv("SOAMTAR_SECRET_KEY"));
// Other base URL, request timeout (default 30 s) and optional request signing
SoamtarClient custom = new SoamtarClient(secretKey, SoamtarClient.DEFAULT_BASE_URL, Duration.ofSeconds(60), true);| Environment variable | Used for |
|---|---|
SOAMTAR_SECRET_KEY | Secret key, when none is passed to the constructor. |
SOAMTAR_API_URL | Base URL (default https://app.so-amtar.com/v1). SOAMTAR_BASE_URL is still read as a fallback (deprecated name). |
Explicit constructor values always win over the environment.
sk_test_ keys run on the same host as live keys (https://app.so-amtar.com/v1): payments are simulated and no money moves. soamtar.isTestMode() tells you which key is in use.
Request signing
With the last constructor argument signRequests = true (new SoamtarClient(null, null, Duration.ofSeconds(30), true)), each POST also carries Soamtar-Timestamp: <unix> and Soamtar-Signature: v1=hex(HMAC_SHA256(secretKey, "<timestamp>.<METHOD>.<path>")), for example over 1700000000.POST.v1/payments (path without the leading slash; timestamp within ±300 s). It is optional hardening: the Bearer key over HTTPS already authenticates you. See Authentication.
Collect a payment
import com.soamtar.sdk.SoamtarException;
import java.util.Map;
try {
Map<String, Object> response = soamtar.payments.create(Map.of(
"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, provider orange_money_sl by default
@SuppressWarnings("unchecked")
Map<String, Object> payment = (Map<String, Object>) response.get("data");
payment.get("status"); // "pending"
payment.get("payment_url"); // hosted checkout page
} catch (SoamtarException e) {
System.err.println(e.getHttpStatus() + " " + e.getCode() + " " + e.getMessage());
}merchant_ref and amount are checked before anything is sent (IllegalArgumentException).
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). Verify it the same way.
Pass your own Idempotency-Key to make retries safe:
soamtar.payments.create(params, "order-1042-attempt-1");Other calls:
soamtar.payments.retrieve(id); // by SOAMTAR id or reference
soamtar.payments.retrieve(id, true); // long poll until a final status
soamtar.payments.verify(id); // re-check the status with Orange Money
soamtar.payments.list(Map.of("status", "success", "per_page", 50));
soamtar.payments.status("TXN-1A2B3C4D-..."); // by SOAMTAR id or reference
soamtar.balance.retrieve(); // available, pending, reservedSend a payout
Payouts require a key with the payout permission. merchant_ref, amount and recipient.phone (+232 followed by 8 digits) are validated before sending.
soamtar.payouts.create(Map.of(
"merchant_ref", "PAYOUT-77",
"amount", "50.00",
"recipient", Map.of("phone", "+23276123456", "name", "Aminata Kamara")
));
soamtar.payouts.retrieve(id);
soamtar.payouts.list(Map.of("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.
Map<String, Object> response = soamtar.refunds.create(Map.of(
"transaction_id", paymentId, // SOAMTAR id (or reference) of the payment
"amount", "50.00", // optional: omit for a full refund of what remains
"reason", "customer_request", // optional
"description", "Item out of stock" // optional
), "refund-order-1042-1"); // Idempotency-Key
Map<String, Object> refund = (Map<String, Object>) response.get("data");
refund.get("reference"); // "REF-…"
refund.get("status"); // "success" (test key), "processing" (Orange Money) or "failed"
soamtar.refunds.retrieve("REF-…"); // id or reference
soamtar.refunds.list(Map.of("transaction_id", paymentId, "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.
Spring Boot
import com.soamtar.sdk.Webhook;
@RestController
public class SoamtarWebhookController {
@Value("${soamtar.webhook-secret}")
private String webhookSecret;
@PostMapping("/webhooks/soamtar")
@SuppressWarnings("unchecked")
public ResponseEntity<String> handle(@RequestBody String payload,
@RequestHeader(Webhook.SIGNATURE_HEADER) String signature) {
try {
Map<String, Object> event = Webhook.constructEvent(payload, signature, webhookSecret);
Map<String, Object> data = (Map<String, Object>) event.get("data");
switch ((String) event.get("event")) {
case "payment.success" -> markPaid((String) data.get("merchant_reference"));
case "payment.failed", "payment.expired", "payment.cancelled" -> markFailed((String) data.get("merchant_reference"));
case "refund.success" -> markRefunded((String) data.get("merchant_reference"), (String) data.get("amount"));
default -> { } // webhook.test and others: just 200
}
return ResponseEntity.ok("OK");
} catch (Webhook.WebhookSignatureException e) {
return ResponseEntity.badRequest().body("Invalid signature");
}
}
}Take the body as a String (@RequestBody String) so the signature is checked against the raw bytes. The 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 soamtar.webhooks.sendTest() and the dashboard's test button whatever your subscriptions. See Webhooks.
Manage endpoints
soamtar.webhooks.create("https://shop.example.sl/webhooks/soamtar"); // default events
soamtar.webhooks.create("https://shop.example.sl/webhooks/soamtar",
List.of("payment.success", "payment.failed", "refund.success", "refund.failed"));
soamtar.webhooks.list();
soamtar.webhooks.secret(endpointId); // signing secret
soamtar.webhooks.sendTest(); // sends a webhook.test eventWithout an event list (or with null / an empty list), create() subscribes to payment.success, payment.failed, payout.success and payout.failed (Webhooks.DEFAULT_EVENTS), like the other SDKs.
Errors
API errors throw SoamtarException: getHttpStatus(), getCode() (for example validation_error) and getBody() (decoded JSON). Network failures use status 0 and code network_error. An invalid key or a double amount throws IllegalArgumentException.
Security
- Native
java.net.http.HttpClient, no third-party HTTP library. - Constant-time signature comparison.
- The secret key is masked in
toString().