A type-safe, fluent Java client for the Ecocash Open API. Supports payment initiation, transaction status polling, and refunds against both sandbox and live environments.
Requires Java 17+ and
org.json:json:20240303.
- Installation
- Quick Start
- Building a Client
- Initiating a Payment
- Polling for Transaction Status
- Single Lookup (No Polling)
- Refunding a Payment
- Error Handling
- Going Live
- Class Reference
- Package Structure
Add the dependency to your build file.
Maven
<dependency>
<groupId>io.github.kinsleykajiva</groupId>
<artifactId>ecocash</artifactId>
<version>0.1.3</version>
</dependency>Gradle
implementation 'io.github.kinsleykajiva:ecocash:0.1.3'The project uses a Maven multi-module structure. You can build and install the SDK to your local repository:
mvn clean installtry (EcocashClient client = EcocashClient.builder()
.apiKey("your-api-key")
.merchantCode("your-merchant-code")
.build()) {
// 1. Initiate a payment — sends a prompt to the customer's handset
InitPaymentResponse payment =
client.initPayment("26377800000", 20.05, "bread");
// 2. Poll until SUCCESS or timeout
LookupTransactionResponse result = client.pollTransaction(payment);
if (result.isPaymentSuccess()) {
System.out.println("Confirmed: " + result.getEcocashReference());
}
}Use the fluent builder. Only apiKey and merchantCode are required.
EcocashClient client = EcocashClient.builder()
.apiKey("your-api-key")
.merchantCode("your-merchant-code")
.build();EcocashClient implements AutoCloseable, so the recommended pattern is try-with-resources:
try (EcocashClient client = EcocashClient.builder()
.apiKey("your-api-key")
.merchantCode("your-merchant-code")
.build()) {
// use client
}| Method | Default | Description |
|---|---|---|
apiKey(String) |
— | Required. Your Ecocash API key. |
merchantCode(String) |
— | Required. Your registered merchant code. |
liveMode() |
sandbox | Switches to the live environment. |
connectTimeoutSeconds(int) |
30 | TCP connect timeout. |
requestTimeoutSeconds(int) |
60 | Per-request read timeout. |
InitPaymentResponse payment = client.initPayment(
"26377858888", // customer MSISDN
20.05, // amount (USD)
"bread" // reason shown on customer's handset
);
System.out.println(payment.getSourceReference()); // UUID — use this to poll
System.out.println(payment.getStatus()); // initial gateway statusinitPayment validates inputs immediately — before any network call:
client.initPayment("", 10.00, "reason"); // throws IllegalArgumentException — blank phone
client.initPayment("2637", -1.00, "reason"); // throws IllegalArgumentException — negative amount
client.initPayment("2637", 10.00, ""); // throws IllegalArgumentException — blank reasonPass the InitPaymentResponse directly from initPayment into any of the pollTransaction overloads. Polling stops as soon as the status is SUCCESS, or throws PollTimeoutException when all attempts are exhausted.
The simplest overload. Uses a fixed 1-second interval, up to 10 attempts.
try {
LookupTransactionResponse result = client.pollTransaction(payment);
if (result.isPaymentSuccess()) {
System.out.println("Paid via: " + result.getEcocashReference());
}
} catch (PollTimeoutException e) {
System.err.println("Customer did not respond in time.");
System.err.println("Last status: " + e.getLastResponse().getStatus());
}Progressively increases the delay after each attempt — ideal for slow approvals and production environments where you want to reduce gateway load.
PollOptions options = PollOptions.builder()
.sleepMs(2_000) // start at 2 s
.multiplier(2) // double each attempt: 2 s → 4 s → 8 s → 16 s …
.maxAttempts(5)
.build();
LookupTransactionResponse result =
client.pollTransaction(payment, PollStrategy.BACKOFF, options);Polls in a tight loop with no delay between attempts. Only use this in sandbox or test environments.
PollOptions options = PollOptions.builder()
.maxAttempts(5)
.build();
LookupTransactionResponse result =
client.pollTransaction(payment, PollStrategy.SIMPLE, options);| Method | Default | Description |
|---|---|---|
maxAttempts(int) |
10 | Maximum number of polls before timeout. |
sleepMs(long) |
1000 | Initial sleep between polls (ms). |
multiplier(int) |
2 | Back-off multiplier. Only used with BACKOFF. |
Use lookupTransaction when you manage the retry loop yourself — for example inside a scheduled job or webhook-driven flow.
LookupTransactionResponse tx =
client.lookupTransaction(
"325a802f-943e-47c2-addf-010285f09cea", // sourceReference
"26377000000" // customer phone
);
// Use the STATUS_* constants to avoid hardcoded strings
switch (tx.getStatus()) {
case LookupTransactionResponse.STATUS_SUCCESS:
handleSuccess(tx);
break;
case LookupTransactionResponse.STATUS_PENDING_VALIDATION:
scheduleRetry(tx.getReference());
break;
default:
log.warn("Unexpected status: {}", tx.getStatus());
}Build the refund parameters with RefundDetails.builder(), then call refundPayment.
RefundDetails details = RefundDetails.builder()
.reference("MP250908.1537.A22242") // original Ecocash transaction reference
.phone("26377000000") // customer MSISDN
.amount(20.05)
.clientName("Acme Grocery Store")
.reason("Defective product — complaint #1234")
// .currency("USD") // optional, defaults to USD
.build();
RefundResponse refund = client.refundPayment(details);
if (refund.isCompleted()) {
System.out.println("Refund processed: " + refund.getEcocashReference());
} else {
System.out.println("Status: " + refund.getTransactionStatus());
}All fields except currency are required. build() throws NullPointerException or IllegalArgumentException if any constraint is violated before a network call is made.
All SDK methods declare throws EcocashException. Catch the most specific type first.
try {
InitPaymentResponse payment = client.initPayment(phone, amount, reason);
LookupTransactionResponse result = client.pollTransaction(payment);
} catch (PollTimeoutException e) {
// Polling ran out of attempts — inspect the last known state
LookupTransactionResponse last = e.getLastResponse();
System.err.println("Timed out. Last status: " + last.getStatus());
System.err.println("Reference: " + last.getReference());
} catch (EcocashApiException e) {
// The Ecocash gateway returned a non-2xx HTTP response
System.err.println("HTTP " + e.getHttpStatus() + ": " + e.getResponseBody());
// 401 → invalid API key
// 429 → rate limited
// 5xx → gateway error
} catch (EcocashException e) {
// Network timeout, parse failure, interrupted thread, etc.
System.err.println("SDK error: " + e.getMessage());
}EcocashException
├── EcocashApiException — non-2xx HTTP response; carries httpStatus + responseBody
└── PollTimeoutException — polling exhausted all attempts; carries lastResponse
IllegalArgumentException is thrown synchronously for invalid inputs (blank phone, negative amount, etc.) and does not extend EcocashException — no network call is ever made when this fires.
Call .liveMode() on the builder. Everything else stays the same.
EcocashClient client = EcocashClient.builder()
.apiKey("your-live-api-key")
.merchantCode("your-live-merchant-code")
.liveMode()
.build();To check at runtime:
client.isLiveMode(); // true
client.getMode(); // "live"The main entry point. Thread-safe after construction; share a single instance across threads.
| Method | Returns | Description |
|---|---|---|
builder() |
Builder |
Creates a new client builder. |
initPayment(phone, amount, reason) |
InitPaymentResponse |
Sends a C2B payment prompt. |
lookupTransaction(reference, phone) |
LookupTransactionResponse |
Single status check, no retry. |
pollTransaction(initResponse) |
LookupTransactionResponse |
Poll with default INTERVAL strategy. |
pollTransaction(initResponse, strategy, options) |
LookupTransactionResponse |
Poll with custom strategy and options. |
refundPayment(details) |
RefundResponse |
Requests a refund. |
isLiveMode() |
boolean |
Returns true when in live mode. |
getMode() |
String |
Returns "sandbox" or "live". |
close() |
void |
Releases HTTP client resources. |
Immutable receipt returned by initPayment. Pass it to pollTransaction or lookupTransaction.
| Method | Description |
|---|---|
getPhone() |
Customer MSISDN. |
getAmount() |
Payment amount. |
getCurrency() |
Currency code, typically "USD". |
getReason() |
Payment description. |
getSourceReference() |
UUID reference for subsequent lookups. |
getStatus() |
Initial gateway status (may be null). |
getEcocashReference() |
Ecocash-internal reference (may be null initially). |
Immutable transaction status returned by lookup and polling calls.
| Method | Description |
|---|---|
isPaymentSuccess() |
true when status is SUCCESS. |
getStatus() |
Raw status string from the gateway. |
getReference() |
Source reference UUID. |
getEcocashReference() |
Ecocash-internal reference, e.g. "MP250908.1537.A22242". |
getCustomerMsisdn() |
Customer MSISDN as reported by the gateway. |
getAmount() |
TransactionAmount (amount + currency), or null. |
getTransactionDateTime() |
Datetime string, e.g. "2025-09-08 15:37:16". |
Known status constants:
LookupTransactionResponse.STATUS_SUCCESS // "SUCCESS"
LookupTransactionResponse.STATUS_PENDING_VALIDATION // "PENDING_VALIDATION"Immutable input object for refundPayment. Always build via RefundDetails.builder().
| Builder Method | Required | Description |
|---|---|---|
reference(String) |
Yes | Original Ecocash transaction reference. |
phone(String) |
Yes | Customer MSISDN. |
amount(double) |
Yes | Amount to refund (must be > 0). |
clientName(String) |
Yes | Merchant client name. |
reason(String) |
Yes | Human-readable reason for the refund. |
currency(String) |
No | Defaults to "USD". |
Immutable result of a refund request.
| Method | Description |
|---|---|
isCompleted() |
true when transactionStatus equals "COMPLETED". |
getTransactionStatus() |
Raw status string, typically "COMPLETED". |
getEcocashReference() |
Ecocash-internal reference for this refund. |
getAmount() |
Refunded amount. |
getCurrency() |
Currency code. |
getSourceReference() |
Source reference echoed from the original payment. |
getResponseMessage() |
Human-readable message from the gateway. |
| Value | Behaviour |
|---|---|
INTERVAL |
Fixed delay of sleepMs between every attempt. Default. |
BACKOFF |
Delay starts at sleepMs and is multiplied by multiplier after each attempt. |
SIMPLE |
No delay between attempts. Use only in sandbox/tests. |
core-lib/
└── src/main/java/ecocash/api/
├── EcocashClient.java ← main entry point
├── EcocashException.java ← exception hierarchy
├── InitPaymentResponse.java ← payment receipt
├── LookupTransactionResponse.java ← transaction status
├── RefundDetails.java ← refund input (builder)
├── RefundResponse.java ← refund result
├── PollStrategy.java ← polling strategy enum
├── PollOptions.java ← polling configuration (builder)
└── package-info.java ← package-level Javadoc
demo/
└── src/main/java/io/github/kinsleykajiva/demo/
└── DemoApp.java ← runnable usage examples
See LICENSE for details.