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.
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.2.1</version>
</dependency>Il nécessite Java 17 ou une version plus récente, et installe OkHttp 4.12.0 et jackson-databind
2.18.2 avec lui. Si votre application fixe déjà leur version, les règles habituelles de gestion des
dépendances s'appliquent.
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 |
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. 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.
getAmount est un nombre entier
Payment décode six champs, et getAmount() renvoie un long, ce qui convient au XAF, au XOF et
à toutes les devises sans décimales. Pour une devise avec décimales, comme 12,50 GHS, lisez
payment.getRaw().get("amount").
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, 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/ConstructEvents'exécute en local, sans appel HTTP. Tous les autres services appellent l'API REST.links,invoices,taxetshieldne fonctionnent qu'en live. Une clé sandbox reçoit403 This feature is only available in live mode.sur chacune de leurs méthodes.refundsettransfersne proposent que la création, la lecture et la liste.
Parcourir une liste par pages
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();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
Les requêtes en échec sont retentées automatiquement, et une nouvelle tentative ne crée jamais rien en double. Si la même commande peut être soumise à nouveau, passez votre propre clé.
Payment payment = wajub.payments().create(
params,
new RequestOptions().setIdempotencyKey("order-" + orderId));Pour définir vos propres 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 reçoit l'identifiant du compte connecté. 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
Si Jackson désérialise le corps dans une classe puis le sérialise de nouveau, les octets changent,
et la signature ne correspond alors plus. @RequestBody byte[] vous donne le corps tel qu'il a
été livré, 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 le premier message de chaque champ.
Tester sans appeler l'API
Fournissez au builder un OkHttpClient dont l'intercepteur répond avant le départ de l'appel, et
tout le SDK fonctionne 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/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());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 Mobile Money. 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.
- 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.