Aller au contenu

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.

Version
1.1.1
Runtime
.NET 8+

Couvre

  • Payments
  • Billing
  • Transfers
  • Sync
  • Shield
  • Tax

Installer

dotnet add package Wajub

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.

Deux façons de créer le client
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
ApiKeyVotre clé sk. ou sk_test.. Utilise WAJUB_API_KEY comme valeur de repli
WebhookSecretSecret whsec_. Utilise WAJUB_WEBHOOK_SECRET comme valeur de repli
HttpClientVotre 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.

Enregistrement dans ASP.NET Core
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"),
    }));

Premier appel

Chaque méthode est asynchrone, se termine par Async et accepte un CancellationToken.

Créer un paiement
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.

Toutes les valeurs au-delà des six propriétés décodées
var reference = payment.Raw.GetValueOrDefault("reference")?.ToString();
var createdAt = payment.Raw.GetValueOrDefault("created_at")?.ToString();

Tous les services du client

ServiceMéthodes
client.GlobalPingAsync, ChannelsAsync, CountriesAsync, CurrenciesAsync
client.PaymentsCreateAsync, RetrieveAsync, ListAsync, CancelAsync, ProcessAsync, ProcessSplitAsync, ListRefundsAsync
client.CustomersCreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, BlockAsync, UnblockAsync, ActivateAsync, DeactivateAsync, ListTaxIdsAsync, CreateTaxIdAsync, DeleteTaxIdAsync
client.RefundsCreateAsync, RetrieveAsync, ListAsync
client.TransfersCreateAsync, RetrieveAsync, ListAsync
client.BeneficiariesCreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync
client.LinksCreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync
client.InvoicesCreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, SendAsync, MarkPaidAsync, CancelAsync
client.AccountsCreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, RegenerateTokenAsync
client.WebhookEndpointsCreateAsync, RetrieveAsync, UpdateAsync, DeleteAsync, ListAsync, RotateSecretAsync
client.BalanceRetrieveAsync
client.EventsListAsync, RetrieveAsync, ResendAsync
client.DisputesListAsync, RetrieveAsync, SubmitEvidenceAsync, AcceptAsync, CloseAsync, SendMessageAsync
client.IdentityResolveAsync, ValidateAsync
client.TaxGetSettingsAsync, UpdateSettingsAsync, RatesAsync, CalculateAsync, ReportsAsync, ListCodesAsync, RetrieveCodeAsync, ListRegistrationsAsync, CreateRegistrationAsync, RetrieveRegistrationAsync, UpdateRegistrationAsync, DeleteRegistrationAsync, JurisdictionsAsync, ThresholdsAsync, ThresholdAlertsAsync
client.ShieldGetSettingsAsync, UpdateSettingsAsync, StatsAsync, ListBlocklistAsync, AddToBlocklistAsync, RemoveFromBlocklistAsync
client.WebhooksConstructEvent
  • webhooks / ConstructEvent s'exécute en local, sans appel HTTP. Tous les autres services appellent l'API REST.
  • links, invoices, tax et shield ne fonctionnent qu'en live. Une clé sandbox reçoit 403 This feature is only available in live mode. sur chacune de leurs méthodes.
  • refunds et transfers ne proposent que la création, la lecture et la liste.

Parcourir une liste paginée

Une page ou toutes les pages
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é.

Un numéro de commande constitue la meilleure 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é

Un appel pour un vendeur
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.

Un endpoint API minimal
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();
});

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

Intercepter d'abord l'erreur précise
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;
}
ClasseCause
AuthenticationError401
PermissionError403
NotFoundError404
InvalidRequestError400 et 422
RateLimitError429, avec RetryAfter en secondes
WajubErrorTout autre statut et classe parente de toutes les erreurs ci-dessus
ApiConnectionErrorAucune 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.

Un test xUnit sans réseau
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

Requête push Mobile Money
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

Un transfert vers un numéro de téléphone
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

Deux terminaux
wajub listen --forward-to localhost:5000/webhooks/wajub
wajub trigger payment.succeeded

Consultez la CLI pour plus de détails.

Que pensez-vous de ce contenu ?