Java
The com.wajub wajub-java artifact, its services, and its unchecked exceptions.
wajub-java is the server side of a Wajub integration. It holds your secret key, creates
payments, reads their real status and verifies webhook signatures.
com.wajub:wajub-java
Stable · GAMaven Central
- Version
- 1.1.1
- Runtime
- Java 17+, OkHttp 4.12, Jackson 2.18
Covers
- Payments
- Billing
- Transfers
- Sync
- Shield
- Tax
Install
<dependency>
<groupId>com.wajub</groupId>
<artifactId>wajub-java</artifactId>
<version>1.2.1</version>
</dependency>It needs Java 17 or later, and brings OkHttp 4.12.0 and jackson-databind 2.18.2 with it. If
your application already pins either one, the usual dependency management applies.
Create the client
Wajub.create is the entry point. It falls back to the environment, so the short form is enough
in most applications.
import com.wajub.Wajub;
// Reads WAJUB_API_KEY and WAJUB_WEBHOOK_SECRET when the field is blank.
Wajub wajub = Wajub.create(Wajub.Config.builder().build());
// Or spell everything out.
Wajub wajub = Wajub.create(
Wajub.Config.builder()
.apiKey(System.getenv("WAJUB_API_KEY"))
.webhookSecret(System.getenv("WAJUB_WEBHOOK_SECRET"))
.build());
// Or pass just the key.
Wajub wajub = Wajub.create(System.getenv("WAJUB_API_KEY"));| Builder method | What it does |
|---|---|
apiKey | Your sk. or sk_test. key. Falls back to WAJUB_API_KEY |
webhookSecret | The whsec_ secret. Falls back to WAJUB_WEBHOOK_SECRET |
httpClient | Your own OkHttpClient, for an interceptor, a proxy or tracing |
A blank key raises IllegalArgumentException: wajub: api key is required at construction, so a
bad configuration fails at start-up rather than on the first payment.
@Configuration
public class WajubConfiguration {
@Bean
Wajub wajub(@Value("${wajub.api-key}") String apiKey,
@Value("${wajub.webhook-secret}") String webhookSecret) {
return Wajub.create(
Wajub.Config.builder()
.apiKey(apiKey)
.webhookSecret(webhookSecret)
.build());
}
}The client is thread safe, so one singleton for the application is the right shape.
The first call
import com.wajub.Payment;
Payment payment = wajub.payments().create(
Map.of(
"amount", 25000,
"currency", "XAF",
"email", "amina@example.com",
"description", "Order 4172",
"reference", "order-4172",
"callback", "https://shop.example.com/complete"),
null);
return "redirect:" + payment.getAuthorizationUrl();Resources are methods, not fields, so it is wajub.payments() and not wajub.payments.
Parameters go in as a Map<String, Object> with the API's exact field names.
RequestOptions has no overload, pass null
Every mutating method takes options as its last argument and there is no two argument version.
null is the ordinary value, and Map.of refuses a null value, so use HashMap when a field
may be absent.
getAmount is a whole number
Payment decodes six fields, and getAmount() returns a long, which suits XAF, XOF and every
currency without decimals. For a currency with decimals, such as GHS 12.50, read
payment.getRaw().get("amount").
String reference = (String) payment.getRaw().get("reference");
Number amount = (Number) payment.getRaw().get("amount");Amounts are in the major unit
25000 with XAF is twenty-five thousand francs, not two hundred and fifty.
Every service on the client
| Service | Methods |
|---|---|
| client.global() | ping, channels, countries, currencies |
| client.payments() | create, retrieve, list, cancel, process, processSplit, listRefunds |
| client.customers() | create, retrieve, update, delete, list, block, unblock, activate, deactivate, listTaxIds, createTaxId, deleteTaxId |
| client.refunds() | create, retrieve, list |
| client.transfers() | create, retrieve, list |
| client.beneficiaries() | create, retrieve, update, delete, list |
| client.links() | create, retrieve, update, delete, list |
| client.invoices() | create, retrieve, update, delete, list, send, markPaid, cancel |
| client.accounts() | create, retrieve, update, delete, list, regenerateToken |
| client.webhookEndpoints() | create, retrieve, update, delete, list, rotateSecret |
| client.balance() | retrieve |
| client.events() | list, retrieve, resend |
| client.disputes() | list, retrieve, submitEvidence, accept, close, sendMessage |
| client.identity() | resolve, validate |
| client.tax() | getSettings, updateSettings, rates, calculate, reports, listCodes, retrieveCode, listRegistrations, createRegistration, retrieveRegistration, updateRegistration, deleteRegistration, jurisdictions, thresholds, thresholdAlerts |
| client.shield() | getSettings, updateSettings, stats, listBlocklist, addToBlocklist, removeFromBlocklist |
| client.webhooks() | constructEvent |
webhooks/ConstructEventruns locally, no HTTP call. All other services call the merchant REST API.links,invoices,taxandshieldare live only. A sandbox key gets403 This feature is only available in live mode.on every one of their methods.refundsandtransfersare create, retrieve and list only.
Paging through a list
PagedResult page = wajub.payments().list(Map.of("status", "succeeded", "per_page", 50));
for (Map<String, Object> payment : page.getData()) {
log.info("{} {}", payment.get("id"), payment.get("amount"));
}
if (page.hasMore()) {
page = page.getNextPage();
}
// Or collect every page at once.
List<Map<String, Object>> all = page.autoPaging();Rows are Map<String, Object>, not typed objects. autoPaging() buys convenience with memory,
since it holds every row before returning.
Idempotency and retries
Failed requests are retried automatically, and a retry never creates anything twice. When the same order may be submitted again, pass your own key.
Payment payment = wajub.payments().create(
params,
new RequestOptions().setIdempotencyKey("order-" + orderId));To set your own timeouts, hand the builder your own OkHttpClient.
OkHttpClient http = new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(60))
.addInterceptor(new TracingInterceptor())
.build();
Wajub wajub = Wajub.create(Wajub.Config.builder().httpClient(http).build());Acting for a connected account
wajub.payments().create(
params,
new RequestOptions().setSync(seller.getWajubAccountId()));setSync takes the connected account's id. Setup is on Sync.
Webhooks
constructEvent verifies the signature and returns the parsed event. It takes the body as bytes,
exactly as delivered.
@PostMapping(value = "/webhooks/wajub", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> wajubWebhook(
@RequestBody byte[] body,
@RequestHeader("X-Wajub-Signature") String signature,
@RequestHeader("X-Wajub-Timestamp") String timestamp) {
Map<String, Object> event;
try {
event = wajub.webhooks().constructEvent(body, signature, timestamp, null);
} catch (WebhookSignatureVerificationException e) {
return ResponseEntity.badRequest().build();
}
if ("payment.succeeded".equals(event.get("event"))) {
fulfilmentQueue.submit(event);
}
return ResponseEntity.ok().build();
}Bind byte[], never a DTO
Letting Jackson deserialise the body into a class and serialising it again changes the bytes, so
the signature stops matching. @RequestBody byte[] hands you the body as delivered, which is what
constructEvent wants.
A null tolerance means the default 300 second window, not an absent check. There is no way to
disable the replay protection, which is deliberate.
The event name is in event, not in type, and the result is a Map<String, Object>. Details on
Signature verification.
Errors
import com.wajub.exception.*;
try {
wajub.payments().create(params, null);
} catch (InvalidRequestException e) {
// e.getErrors() is {"amount": "The amount must be at least 25."}
return ResponseEntity.unprocessableEntity().body(e.getErrors());
} catch (RateLimitException e) {
return ResponseEntity.status(503)
.header("Retry-After", String.valueOf(e.getRetryAfter()))
.build();
} catch (WajubException e) {
log.error("wajub failed code={} status={}", e.getCode(), e.getHttpStatus());
throw e;
}Class in com.wajub.exception | Thrown on |
|---|---|
AuthenticationException | 401 |
PermissionException | 403 |
NotFoundException | 404 |
InvalidRequestException | 400 and 422 |
RateLimitException | 429, with getRetryAfter() in seconds |
WajubException | Every other status, and the parent of all of the above |
ApiConnectionException | No response at all, network or timeout |
WebhookSignatureVerificationException | A webhook that did not verify |
All of them expose getMessage(), getCode(), getHttpStatus(), getErrors() and getRaw().
These are unchecked, despite the throws clause
WajubException extends RuntimeException, so the throws WajubException on every method is
documentation rather than a compiler obligation. Nothing forces you to handle a failed payment,
and an uncaught one becomes a 500. Catch it deliberately.
getErrors() returns Map<String, String>, with the first message of each field.
Testing without touching the API
Hand the builder an OkHttpClient whose interceptor answers before the call leaves, and the whole
SDK runs offline.
Interceptor stub = chain -> new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(201)
.message("Created")
.body(ResponseBody.create("""
{"code":201,
"authorization_url":"https://pay.wajub.com/test_xxxxx",
"authorization_token":"test_xxxxx",
"transaction":{"id":"trx_test","status":"pending","amount":25000}}
""", MediaType.get("application/json")))
.build();
Wajub wajub = Wajub.create(
Wajub.Config.builder()
.apiKey("sk_test.fake")
.httpClient(new OkHttpClient.Builder().addInterceptor(stub).build())
.build());
Payment payment = wajub.payments().create(Map.of("amount", 25000, "currency", "XAF"), null);
assertEquals("test_xxxxx", payment.getAuthorizationToken());Charging without the hosted page
Payment payment = wajub.payments().create(
Map.of("amount", 25000, "currency", "XAF", "phone", "+237670000000"),
null);
wajub.payments().process(
payment.getId(),
Map.of("channel", "cm.mtn", "phone", "+237670000000"),
null);A channel is country.operator, so cm.mtn is MTN Mobile Money in Cameroon. The full list is on
Mobile Money. The payer still approves on their handset, so the
outcome arrives on the webhook.
Paying out
Map<String, Object> transfer = wajub.transfers().create(
Map.of(
"amount", 100000,
"currency", "XAF",
"beneficiary", Map.of(
"name", "Amina Diallo",
"channel", "cm.mtn",
"phone", "+237670000000"),
"reference", "payout-892"),
new RequestOptions().setIdempotencyKey("payout-892"));beneficiary also takes the ben_… id of a saved beneficiary, which is the better shape once you
pay the same person twice.
Local development
wajub listen --forward-to localhost:8080/webhooks/wajub
wajub trigger payment.succeededMore on the CLI.
Related pages
- SDK QuickstartThe same first call, in five languages side by side.
- WebhooksEvery event, and the delivery guarantees behind them.
- Naming conventionsWhy Java exposes camelCase over a snake_case wire.
- Error handlingWhich failures to retry and which to surface.
- API referenceThe endpoints behind every method above.