C# / .NET
Le package NuGet Wajub, ses services et l'interface asynchrone qu'il expose.
Le package Wajub 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. Les types
de référence nullables sont activés. Chaque appel est awaitable et accepte un CancellationToken.
Wajub
Stable · GANuGet
- Version
- 1.1.1
- Runtime
- .NET 8+, no package dependency
Couvre
- Payments
- Billing
- Transfers
- Sync
- Shield
- Tax
Installer
dotnet add package WajubL'assembly cible net8.0 et utilise System.Text.Json et HttpClient du framework. Aucune autre
dépendance n'est nécessaire.
Créer le client
WajubClient.Create est le point d'entrée. Il utilise l'environnement comme valeur de repli. Une
configuration vide suffit donc généralement.
using Wajub;
// Reads WAJUB_API_KEY and WAJUB_WEBHOOK_SECRET when the property is null.
var wajub = WajubClient.Create(new WajubConfig());
// Or spell it out.
var wajub = WajubClient.Create(new WajubConfig
{
ApiKey = Environment.GetEnvironmentVariable("WAJUB_API_KEY"),
WebhookSecret = Environment.GetEnvironmentVariable("WAJUB_WEBHOOK_SECRET"),
});| Propriété | Fonction |
|---|---|
ApiKey | Votre clé sk. ou sk_test.. Utilise WAJUB_API_KEY comme valeur de repli |
WebhookSecret | Secret whsec_. Utilise WAJUB_WEBHOOK_SECRET comme valeur de repli |
IdempotencyKeyPrefix | Préfixe de la clé générée. "wajub" par défaut |
HttpClient | Votre propre HttpClient, issu de IHttpClientFactory ou d'un handler de test |
Une clé vide génère ArgumentException: wajub: api key is required lors de la construction. Une
mauvaise configuration échoue donc au démarrage.
builder.Services.AddHttpClient("wajub");
builder.Services.AddSingleton(sp =>
WajubClient.Create(new WajubConfig
{
ApiKey = builder.Configuration["Wajub:ApiKey"],
WebhookSecret = builder.Configuration["Wajub:WebhookSecret"],
HttpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient("wajub"),
}));Utilisez un singleton sans le libérer à chaque requête
WajubClient implémente IDisposable et possède le HttpClient qu'il crée. Son enregistrement
comme singleton est correct. Une instruction using dans un handler de requête libère le pool de
sockets à chaque appel et provoque un épuisement des ports, erreur classique avec HttpClient.
Premier appel
Chaque méthode est asynchrone et se termine par Async.
var payment = await wajub.Payments.CreateAsync(new Dictionary<string, object?>
{
["amount"] = 25000,
["currency"] = "XAF",
["email"] = "amina@example.com",
["description"] = "Order 4172",
["reference"] = $"order-{orderId}",
["callback"] = "https://shop.example.com/complete",
}, cancellationToken: ct);
return Results.Redirect(payment.AuthorizationUrl);Les paramètres sont transmis dans un Dictionary<string, object?> avec les noms de champs exacts
de l'API. Le résultat est un record, donc immuable et comparé par valeur.
Payment.Amount est un long, mais l'API envoie une valeur décimale
Payment décode six propriétés et Amount est un long. Les montants utilisent l'unité principale.
12,50 GHS devient donc 12. Cette conversion est sûre pour XAF, XOF et les devises sans décimales,
mais perd de l'information pour les autres. Pour une devise décimale, lisez payment.Raw["amount"].
var reference = payment.Raw.GetValueOrDefault("reference")?.ToString();
var createdAt = payment.Raw.GetValueOrDefault("created_at")?.ToString();Tous les services du client
| Service | Méthodes |
|---|---|
| client.Global | PingAsync, ChannelsAsync, CountriesAsync, CurrenciesAsync |
| client.Payments | CreateAsync, InitializeAsync, RetrieveAsync, ListAsync, CancelAsync, ProcessAsync, ProcessSplitAsync, ListRefundsAsync |
| client.Customers | CreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, BlockAsync, UnblockAsync, ActivateAsync, DeactivateAsync, ListTaxIdsAsync, CreateTaxIdAsync, DeleteTaxIdAsync |
| client.Refunds | CreateAsync, RetrieveAsync, ListAsync |
| client.Transfers | CreateAsync, RetrieveAsync, ListAsync |
| client.Beneficiaries | CreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync |
| client.Links | CreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync |
| client.Invoices | CreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, SendAsync, MarkPaidAsync, CancelAsync |
| client.Accounts | CreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, RegenerateTokenAsync |
| client.WebhookEndpoints | CreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, RotateSecretAsync |
| client.Balance | RetrieveAsync |
| client.Events | ListAsync, RetrieveAsync, ResendAsync |
| client.Disputes | ListAsync, RetrieveAsync, SubmitEvidenceAsync, AcceptAsync, CloseAsync, SendMessageAsync |
| client.Identity | ResolveAsync, ValidateAsync |
| client.Tax | GetSettingsAsync, UpdateSettingsAsync, RatesAsync, CalculateAsync, ReportsAsync, ListCodesAsync, RetrieveCodeAsync, ListRegistrationsAsync, CreateRegistrationAsync, RetrieveRegistrationAsync, UpdateRegistrationAsync, DeleteRegistrationAsync, JurisdictionsAsync, ThresholdsAsync, ThresholdAlertsAsync |
| client.Shield | GetSettingsAsync, UpdateSettingsAsync, StatsAsync, ListBlocklistAsync, AddToBlocklistAsync, RemoveFromBlocklistAsync |
| client.Listen | ConfigAsync, AuthAsync |
| 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 paginée
var page = await wajub.Payments.ListAsync(new Dictionary<string, object?>
{
["status"] = "success",
["per_page"] = 50,
}, ct);
foreach (var payment in page.Data)
{
logger.LogInformation("{Id} {Amount}", payment["id"], payment["amount"]);
}
if (page.HasMore)
{
page = await page.GetNextPageAsync(ct);
}
// Or collect every page at once.
var all = await page.AutoPagingIterAsync(ct);Les lignes sont des Dictionary<string, object?>, pas des records typés. AutoPagingIterAsync
renvoie une liste plutôt qu'un IAsyncEnumerable. Il conserve donc toutes les lignes en mémoire avant de répondre.
Idempotence et nouvelles tentatives
Chaque POST et PUT contient une valeur Idempotency-Key générée, qui sécurise les nouvelles tentatives automatiques.
var payment = await wajub.Payments.CreateAsync(
parameters,
new RequestOptions { IdempotencyKey = $"order-{orderId}" },
ct);| Élément | Valeur |
|---|---|
| Statuts réessayés | 429, 500, 502, 503, 504 et tout échec réseau |
| Tentatives | 3 au total, un appel initial et deux nouvelles tentatives |
| Attente progressive | Exponentielle avec part d'aléa, en respectant Retry-After sur 429 |
| Délai d'expiration | 30 secondes sur le HttpClient créé par le SDK |
Le nombre de tentatives est fixe. Fournissez votre propre HttpClient pour modifier le délai d'expiration.
N'ajoutez pas Polly par-dessus
Le SDK réessaie déjà les échecs temporaires. Ajouter une règle Polly au HttpClient injecté
multiplie les deux mécanismes. Une réponse 503 peut alors provoquer neuf tentatives au lieu de
trois. Choisissez une seule couche, celle du SDK connaît déjà les clés d'idempotence.
Agir pour un compte connecté
await wajub.Payments.CreateAsync(
parameters,
new RequestOptions { Sync = seller.WajubAccountId },
ct);Sync devient l'en-tête X-Sync. La configuration figure dans Sync.
Webhooks
ConstructEvent vérifie la signature et renvoie l'événement analysé. Il reçoit le corps sous forme
d'octets exactement comme il a été livré. C'est la seule méthode synchrone du client.
app.MapPost("/webhooks/wajub", async (HttpRequest request, WajubClient wajub) =>
{
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
Dictionary<string, object?> evt;
try
{
evt = wajub.Webhooks.ConstructEvent(
buffer.ToArray(),
request.Headers["X-Wajub-Signature"]!,
request.Headers["X-Wajub-Timestamp"]!);
}
catch (WebhookSignatureVerificationError)
{
return Results.BadRequest();
}
if (evt["event"] as string == "payment.succeeded")
{
await queue.EnqueueAsync(evt);
}
return Results.Ok();
});Lisez le flux sans jamais associer un modèle
La signature couvre {timestamp}.{raw body}. Associer le corps à une classe puis le resérialiser
modifie l'ordre et la casse des clés. Le hash ne correspond alors plus. La copie de request.Body
fournit les octets livrés, exactement ce qu'attend ConstructEvent.
ConstructEvent accepte un ReadOnlySpan<byte>. L'appel ne peut donc pas figurer dans une
expression async et le corps est d'abord mis en mémoire tampon. Le TimeSpan par défaut désigne
une fenêtre de 300 secondes, pas une absence de vérification.
Le nom de l'événement se trouve dans event, pas dans type, et le résultat est un dictionnaire.
Consultez Vérification de signature.
Erreurs
try
{
await wajub.Payments.CreateAsync(parameters, cancellationToken: ct);
}
catch (InvalidRequestError e)
{
// e.Errors is {"amount": "The amount must be at least 25."}
return Results.ValidationProblem(
e.Errors?.ToDictionary(x => x.Key, x => new[] { x.Value }) ?? []);
}
catch (RateLimitError e)
{
return Results.StatusCode(503);
}
catch (WajubError e)
{
logger.LogError("wajub failed code={Code} status={Status}", e.Code, e.HttpStatus);
throw;
}| Classe | Cause |
|---|---|
AuthenticationError | 401 |
PermissionError | 403 |
NotFoundError | 404 |
InvalidRequestError | 400 et 422 |
RateLimitError | 429, avec RetryAfter en secondes |
WajubError | Tout autre statut et classe parente de toutes les erreurs ci-dessus |
ApiConnectionError | Aucune réponse, problème réseau ou délai d'expiration |
WebhookSignatureVerificationError | Échec de vérification d'un webhook |
WajubError expose Code, HttpStatus, Errors et Raw.
Errors contient un message par champ, pas une liste
L'API envoie un tableau de messages par champ. Le mapping conserve uniquement le premier.
Errors est donc un Dictionary<string, string>. Les tableaux complets restent disponibles dans Raw["errors"].
ApiConnectionError contient uniquement un message
Contrairement aux autres erreurs, elle ne possède ni statut ni Raw, car aucune réponse n'a été
reçue. Son Code vaut toujours "network_error".
Tester sans appeler l'API
L'URL de base est une constante sans remplacement par l'environnement. Le point de substitution est
donc le HttpClient. Un HttpMessageHandler personnalisé permet d'utiliser tout le SDK hors ligne.
sealed class StubHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct) =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.Created)
{
Content = new StringContent("""
{"code":201,
"authorization_url":"https://pay.wajub.com/tok_test",
"authorization_token":"tok_test",
"transaction":{"id":"trx_test","status":"pending","amount":25000}}
""", Encoding.UTF8, "application/json"),
});
}
var wajub = WajubClient.Create(new WajubConfig
{
ApiKey = "sk_test.fake",
HttpClient = new HttpClient(new StubHandler()),
});
var payment = await wajub.Payments.CreateAsync(
new Dictionary<string, object?> { ["amount"] = 25000, ["currency"] = "XAF" });
Assert.Equal("tok_test", payment.AuthorizationToken);Débiter sans la page hébergée
var payment = await wajub.Payments.CreateAsync(new Dictionary<string, object?>
{
["amount"] = 25000,
["currency"] = "XAF",
["phone"] = "+237670000000",
}, cancellationToken: ct);
await wajub.Payments.ProcessAsync(payment.Id, new Dictionary<string, object?>
{
["channel"] = "cm.mtn",
["phone"] = "+237670000000",
}, cancellationToken: ct);Un canal suit le format country.operator. cm.mtn désigne donc MTN Mobile Money au Cameroun. La
liste complète figure dans Moyens de paiement et canaux. Le payeur doit
encore approuver sur son téléphone. Le résultat arrive donc dans le webhook.
Effectuer un payout
var transfer = await wajub.Transfers.CreateAsync(
new Dictionary<string, object?>
{
["amount"] = 100000,
["currency"] = "XAF",
["beneficiary"] = new Dictionary<string, object?>
{
["name"] = "Amina Diallo",
["channel"] = "cm.mtn",
["phone"] = "+237670000000",
},
["reference"] = "payout-892",
},
new RequestOptions { IdempotencyKey = "payout-892" },
ct);beneficiary accepte aussi l'identifiant ben_… d'un bénéficiaire enregistré. Préférez cette forme
dès que vous payez deux fois la même personne.
Développement local
wajub listen --forward-to localhost:5000/webhooks/wajub
wajub trigger payment.succeededConsultez la CLI pour plus de détails.
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 C# expose du PascalCase au-dessus d'échanges en snake_case.
- IdempotenceCe que protège une clé et pendant combien de temps.
- Gestion des erreursLes échecs à réessayer et ceux à afficher.
- Vue d'ensemble de la référence APILes endpoints utilisés par toutes les méthodes ci-dessus.