Java
L'artefact wajub-java de com.wajub, ses services et ses exceptions non vérifiées.
wajub-java constitue la partie serveur d'une intégration Wajub. Il conserve votre clé secrète,
crée des paiements, lit leur statut réel et vérifie les signatures des webhooks. Il utilise OkHttp
pour le transport et Jackson pour le JSON, rien d'autre.
com.wajub:wajub-java
Stable · GAMaven Central
- Version
- 1.1.1
- Runtime
- Java 17+, OkHttp 4.12, Jackson 2.18
Couvre
- Paiements
- Facturation
- Transferts
- Sync
- Shield
- Taxes
Installation
<dependency>
<groupId>com.wajub</groupId>
<artifactId>wajub-java</artifactId>
<version>1.1.1</version>
</dependency>L'artefact est compilé avec --release 17. Java 17 est donc la version minimale, et toute version
plus récente peut l'exécuter. Il installe transitivement OkHttp 4.12.0 et jackson-databind 2.18.2.
Si votre application fixe déjà leur version, les règles habituelles de gestion des dépendances s'appliquent.
Le groupe est com.wajub
co.wajub ne correspond à rien. Les coordonnées publiées sont com.wajub:wajub-java, et les
artefacts mobiles se trouvent dans le même groupe.
Créer le client
Wajub.create est le point d'entrée. Il utilise les variables d'environnement en repli. La forme
courte suffit donc dans la plupart des 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"));| Méthode du builder | Rôle |
|---|---|
apiKey | Votre clé sk. ou sk_test.. Utilise WAJUB_API_KEY en repli |
webhookSecret | Le secret whsec_. Utilise WAJUB_WEBHOOK_SECRET en repli |
idempotencyKeyPrefix | Préfixe de la clé générée. Valeur par défaut : "wajub" |
httpClient | Votre propre OkHttpClient, pour un intercepteur, un proxy ou le traçage |
Une clé vide déclenche IllegalArgumentException: wajub: api key is required lors de la construction.
Une mauvaise configuration échoue donc au démarrage, pas lors du premier paiement.
@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());
}
}Le client est sûr pour les accès concurrents et contient un pool de connexions OkHttp. Un singleton pour toute l'application est donc le bon choix.
Le premier appel
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();Les ressources sont des méthodes, pas des champs. Utilisez donc wajub.payments() et non
wajub.payments. Les paramètres sont transmis dans un Map<String, Object> avec les noms de
champs exacts de l'API.
RequestOptions n'a pas de surcharge, passez null
Chaque méthode de modification accepte les options comme dernier argument, sans version à deux
arguments. null est la valeur habituelle. Comme Map.of refuse une valeur nulle, utilisez
HashMap lorsqu'un champ peut être absent.
Payment.getAmount est un long alors que l'API envoie un nombre décimal
Payment décode six champs, et getAmount() renvoie un long via Number.longValue(), ce qui
tronque la valeur. 12,50 GHS devient 12. L'accesseur convient au XAF, au XOF et à toutes les
devises sans décimales, mais perd de la précision pour les autres. Pour une devise décimale,
lisez payment.getRaw().get("amount"), qui contient la valeur intacte.
String reference = (String) payment.getRaw().get("reference");
Number amount = (Number) payment.getRaw().get("amount");Les montants utilisent l'unité principale
25000 avec XAF représente vingt-cinq mille francs, pas deux cent cinquante.
Tous les services du client
| Service | Méthodes |
|---|---|
| client.global() | ping, channels, countries, currencies |
| client.payments() | create, initialize, 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.listen() | config, auth |
| 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. The shared CRUD base also exposesupdateanddeleteon them, but the API serves no such route.
Parcourir une liste par pages
PagedResult page = wajub.payments().list(Map.of("status", "success", "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();Les lignes sont des Map<String, Object>, pas des objets typés. autoPaging() simplifie le parcours,
mais consomme plus de mémoire, car il conserve toutes les lignes avant de les renvoyer.
Idempotence et nouvelles tentatives
Chaque requête POST et PUT contient une Idempotency-Key générée. Cette clé sécurise les
nouvelles tentatives automatiques.
Payment payment = wajub.payments().create(
params,
new RequestOptions().setIdempotencyKey("order-" + orderId));| Élément | Valeur |
|---|---|
| Statuts concernés | 429, 500, 502, 503, 504 et toute erreur réseau |
| Tentatives | 3 au total, l'appel initial et deux nouvelles tentatives |
| Attente progressive | Exponentielle avec une part d'aléa (jitter), en respectant Retry-After pour une erreur 429 |
| Délai d'expiration | 30 secondes pour la connexion, la lecture et l'écriture |
Le nombre de nouvelles tentatives est fixe. Pour modifier les délais d'expiration, fournissez
votre propre OkHttpClient au builder.
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());Agir pour un compte connecté
wajub.payments().create(
params,
new RequestOptions().setSync(seller.getWajubAccountId()));setSync devient l'en-tête X-Sync. La configuration est présentée dans Sync.
Webhooks
constructEvent vérifie la signature et renvoie l'événement analysé. Il accepte le corps sous forme
d'octets, exactement tel qu'il a été livré.
@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();
}Liez byte[], jamais un DTO
La signature couvre {timestamp}.{raw body}. Si Jackson désérialise le contenu dans une classe
puis le sérialise de nouveau, l'ordre des clés et les espaces changent. Le hash ne correspond alors
plus. @RequestBody byte[] vous donne les octets tels qu'ils ont été livrés, comme l'exige constructEvent.
Une tolérance null applique la fenêtre par défaut de 300 secondes, elle ne supprime pas la
vérification. Il est volontairement impossible de désactiver la protection contre les rejeux.
Le nom de l'événement se trouve dans event, pas dans type, et le résultat est un
Map<String, Object>. Consultez Vérification de signature.
Erreurs
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;
}Classe dans com.wajub.exception | Cas de déclenchement |
|---|---|
AuthenticationException | 401 |
PermissionException | 403 |
NotFoundException | 404 |
InvalidRequestException | 400 et 422 |
RateLimitException | 429, avec getRetryAfter() en secondes |
WajubException | Tout autre statut. C'est aussi la classe parente de toutes les exceptions ci-dessus |
ApiConnectionException | Aucune réponse, à cause du réseau ou d'un délai d'expiration |
WebhookSignatureVerificationException | Un webhook dont la signature n'a pas été vérifiée |
Toutes exposent getMessage(), getCode(), getHttpStatus(), getErrors() et getRaw().
Ces exceptions ne sont pas vérifiées malgré la clause throws
WajubException étend RuntimeException. Le throws WajubException de chaque méthode sert donc
de documentation, sans obligation imposée par le compilateur. Rien ne vous oblige à gérer un
paiement échoué, et une exception non interceptée devient une erreur 500. Interceptez-la explicitement.
getErrors() renvoie un Map<String, String>, avec un message par champ, alors que l'API envoie
un tableau par champ. Seul le premier message de chaque champ est conservé lors du mapping.
Tester sans appeler l'API
L'URL de base est une constante qui ne peut pas être remplacée par une variable d'environnement.
Vous ne pouvez donc pas la faire pointer vers un serveur simulé. Le point d'interception est
OkHttpClient : un intercepteur qui répond avant le départ de l'appel permet d'utiliser tout le SDK hors ligne.
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/tok_test",
"authorization_token":"tok_test",
"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("tok_test", payment.getAuthorizationToken());Seul le SDK Node.js lit WAJUB_API_URL
@wajub/node permet à cette variable de rediriger l'URL de base, ce qui est pratique avec une
stack locale. Java, Python, PHP, Go, Ruby et C# utilisent tous https://api.wajub.com comme
constante. Pour les intercepter, injectez un transport comme dans l'exemple ci-dessus.
Débiter sans page hébergée
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);Un canal suit le format country.operator. Ainsi, cm.mtn désigne MTN Mobile Money au Cameroun.
La liste complète se trouve dans Moyens de paiement et canaux. Le payeur
doit toujours approuver l'opération sur son téléphone. Le résultat arrive donc par webhook.
Effectuer un payout
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 accepte aussi l'identifiant ben_… d'un bénéficiaire enregistré. Cette forme est
préférable dès que vous payez plusieurs fois la même personne.
Développement local
wajub listen --forward-to localhost:8080/webhooks/wajub
wajub trigger payment.succeededPour en savoir plus, consultez la CLI.
Pages associées
- Démarrage rapide des SDKsLe même premier appel dans cinq langages, présentés côte à côte.
- Android (Kotlin)Les artefacts mobiles du même groupe com.wajub.
- WebhooksTous les événements et leurs garanties de livraison.
- Conventions de nommage des SDKsPourquoi Java expose du camelCase sur un protocole en snake_case.
- Gestion des erreursLes échecs à retenter et ceux à présenter à l'utilisateur.
- Vue d'ensemble de la référence APILes endpoints utilisés par chaque méthode ci-dessus.