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.
Wajub
Stable · GANuGet
- Version
- 1.1.1
- Runtime
- .NET 8+
Couvre
- Payments
- Billing
- Transfers
- Sync
- Shield
- Tax
Installer
dotnet add package WajubCré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 |
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
Enregistrez WajubClient comme singleton. Ne l'enveloppez pas dans un using à l'intérieur d'un
handler de requête : comme tout HttpClient, le libérer à chaque appel épuise les ports du serveur.
Premier appel
Chaque méthode est asynchrone, se termine par Async et accepte un CancellationToken.
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.
Amount est un nombre entier
Payment décode six propriétés, et Amount est 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.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, 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.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 paginée
var page = await wajub.Payments.ListAsync(new Dictionary<string, object?>
{
["status"] = "succeeded",
["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. Il conserve donc toutes les lignes en mémoire avant de répondre.
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é.
var payment = await wajub.Payments.CreateAsync(
parameters,
new RequestOptions { IdempotencyKey = $"order-{orderId}" },
ct);Pour définir votre propre délai d'expiration, fournissez votre propre HttpClient.
N'ajoutez pas Polly par-dessus
Les requêtes en échec sont déjà retentées. N'ajoutez donc aucune règle de nouvelle tentative,
Polly ou une autre, au HttpClient que vous fournissez.
Agir pour un compte connecté
await wajub.Payments.CreateAsync(
parameters,
new RequestOptions { Sync = seller.WajubAccountId },
ct);Sync reçoit l'identifiant du compte connecté. 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é, et il est synchrone.
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
Associer le corps à une classe puis le sérialiser de nouveau modifie les octets, et la signature
ne correspond alors plus. La copie de request.Body fournit le corps tel qu'il a été livré,
exactement ce qu'attend ConstructEvent.
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 de
valeurs simples : string, long ou double, bool, null, un Dictionary<string, object?>
imbriqué ou une List<object?>. evt["event"] as string lit donc le type. Un en-tête de signature
ou d'horodatage absent lève WebhookSignatureVerificationError, comme un en-tête faux. 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. Son Code vaut "network_error" |
WebhookSignatureVerificationError | Échec de vérification d'un webhook |
WajubError expose Code, HttpStatus, Errors et Raw. Errors contient le premier message
de chaque champ, et Raw["errors"] les contient tous.
Tester sans appeler l'API
Fournissez un HttpClient construit sur votre propre HttpMessageHandler, et tout le SDK fonctionne
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/test_xxxxx",
"authorization_token":"test_xxxxx",
"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("test_xxxxx", 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 Mobile Money. 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.