Skip to content

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 · GA

Maven 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.

Two ways to build it
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 methodWhat it does
apiKeyYour sk. or sk_test. key. Falls back to WAJUB_API_KEY
webhookSecretThe whsec_ secret. Falls back to WAJUB_WEBHOOK_SECRET
httpClientYour 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.

As a Spring bean
@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

Create a payment
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.

Everything past the six decoded fields
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

ServiceMethods
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 / ConstructEvent runs locally, no HTTP call. All other services call the merchant REST API.
  • links, invoices, tax and shield are live only. A sandbox key gets 403 This feature is only available in live mode. on every one of their methods.
  • refunds and transfers are create, retrieve and list only.

Paging through a list

One page, or all of them
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.

RequestOptions chains
Payment payment = wajub.payments().create(
    params,
    new RequestOptions().setIdempotencyKey("order-" + orderId));

To set your own timeouts, hand the builder your own OkHttpClient.

Your own OkHttp client
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

One call, one seller
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.

A Spring Boot controller
@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();
}

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

Catch the specific one first
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.exceptionThrown on
AuthenticationException401
PermissionException403
NotFoundException404
InvalidRequestException400 and 422
RateLimitException429, with getRetryAfter() in seconds
WajubExceptionEvery other status, and the parent of all of the above
ApiConnectionExceptionNo response at all, network or timeout
WebhookSignatureVerificationExceptionA webhook that did not verify

All of them expose getMessage(), getCode(), getHttpStatus(), getErrors() and getRaw().

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.

A JUnit test with no network
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

Mobile Money push
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

A transfer to a phone number
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

Two terminals
wajub listen --forward-to localhost:8080/webhooks/wajub
wajub trigger payment.succeeded

More on the CLI.

What did you think of this content?