WayaQuick.Integration 2.0.1

dotnet add package WayaQuick.Integration --version 2.0.1
                    
NuGet\Install-Package WayaQuick.Integration -Version 2.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="WayaQuick.Integration" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="WayaQuick.Integration" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="WayaQuick.Integration" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add WayaQuick.Integration --version 2.0.1
                    
#r "nuget: WayaQuick.Integration, 2.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package WayaQuick.Integration@2.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=WayaQuick.Integration&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=WayaQuick.Integration&version=2.0.1
                    
Install as a Cake Tool

WayaQuick .NET

.NET client for the WayaQuick Merchant API v2. Collect payments, send payouts, verify bank accounts, and run BVN identity checks in Nigeria.

Targets net8.0. No dependencies outside the framework. Server-side only — your secret key must never leave your server.

Install

The package is published to nuget.org as WayaQuick.Integration:

dotnet add package WayaQuick.Integration --version 2.0.1

Offline / air-gapped fallback — install from the repo's artifact feed

Every release is also committed to this repo as a pre-built .nupkg under artifact/version2.0.1/, bundled as a zip for easy download.

Heads up — --source is not a download URL. A NuGet source must be either a local folder or a NuGet feed endpoint (a v2/v3 service index). You cannot pass a GitHub web link or a raw .nupkg URL (e.g. https://github.com/.../WayaQuick.Integration.2.0.1.nupkg) to --source — NuGet can't read a single file over HTTP. So from GitHub you download the zip first (Option 1) or clone the repo (Option 2), then install from a local folder.

Option 1 — Download the zip from GitHub, then install from the folder:

  1. Go to the repo and open the zip: https://github.com/WAYA-MULTI-LINK/WAYA-PAY-CHAT-2.0-.NET-LIBRARY/blob/main/artifact/version2.0.1/WayaQuick.Integration.2.0.1.zip

  2. Click Download raw file (or curl the raw URL), then unzip it:

    curl -L -O \
      https://github.com/WAYA-MULTI-LINK/WAYA-PAY-CHAT-2.0-.NET-LIBRARY/raw/main/artifact/version2.0.1/WayaQuick.Integration.2.0.1.zip
    unzip WayaQuick.Integration.2.0.1.zip -d ./wayaquick-pkg   # -> ./wayaquick-pkg/WayaQuick.Integration.2.0.1.nupkg
    
  3. Install from the folder you unzipped into (the --source is that local folder, not a URL):

    dotnet add package WayaQuick.Integration --version 2.0.1 --source ./wayaquick-pkg
    

Option 2 — Clone the repo (zero config):

The repo ships a nuget.config that registers artifact/ as a source (NuGet searches its subfolders recursively, so the package inside version2.0.1/ is found), so any project built from within the clone resolves the package with no --source flag:

git clone https://github.com/WAYA-MULTI-LINK/WAYA-PAY-CHAT-2.0-.NET-LIBRARY.git
cd WAYA-PAY-CHAT-2.0-.NET-LIBRARY
dotnet add <your-project> package WayaQuick.Integration --version 2.0.1

Consuming from a project outside the clone? Point --source at the cloned artifact/ folder:

dotnet add package WayaQuick.Integration --version 2.0.1 --source /path/to/WAYA-PAY-CHAT-2.0-.NET-LIBRARY/artifact

See artifact/README.md for more.

Quickstart

using WayaQuick;

var client = new WayaQuickClient(new WayaQuickOptions
{
    MerchantId = "MER_...",            // from the dashboard
    SecretKey  = "WAYASECK_TEST_...",  // swap for WAYASECK_... on live
});

Return types at a glance

Call Returns
client.Payouts.ListBanksAsync() Task<List<PayoutBankResponseModel>>
client.Payouts.VerifyAccountAsync(…) Task<PayoutVerifyResponseModel>
client.Payouts.InitiateAsync(…) Task<PayoutResponseModel>
client.Payouts.GetStatusAsync(reference) Task<PayoutStatusModel>
client.Collection.InitiateAsync(…) Task<CollectionResponseModel>
client.Collection.GetStatusAsync(refNo) Task<CollectionStatusModel>
client.Identity.VerifyBvnAsync(…) Task<BvnIdentityResponseModel>
client.Webhooks.ConstructEvent(…) WebhookEvent
client.Webhooks.VerifySignature(…) bool
WayaQuickClient.GenerateReference(prefix) string

Each async method also accepts an optional CancellationToken and is named with the …Async suffix; await it to unwrap the Task<T>.

List banks

var banks = await client.Payouts.ListBanksAsync();

Returns List<PayoutBankResponseModel> — each entry has .Code, .Name, .Id, and .Status.

Verify an account

Always verify before sending a payout — confirms the account exists and returns the registered name.

var account = await client.Payouts.VerifyAccountAsync(new()
{
    AccountNumber = "0123456789",
    EnquiryType   = "OTHERS",   // "WAYA-BANK" for intra-bank
    BankCode      = "044",      // required when EnquiryType is "OTHERS"
});
Console.WriteLine(account.AccountName); // "JOHN DOE"

Returns PayoutVerifyResponseModel.Successful, .AccountNumber, .AccountName, .BankCode, .BankName, .ResponseCode, .ResponseMessage, .EnquiryType.

Initiate a payout

var payout = await client.Payouts.InitiateAsync(new()
{
    Amount        = 5000.00m,
    Currency      = "NGN",
    AccountNumber = "0123456789",
    BankCode      = "044",
    AccountName   = account.AccountName,
    Reference     = WayaQuickClient.GenerateReference("PAYOUT"),
    Narration     = "April salary",
});
// payout.Status == "PROCESSING" means accepted, not yet settled

Returns PayoutResponseModel.PayoutReference, .MerchantReference, .Status, .Message.

GenerateReference produces a timestamped, collision-resistant key (PAYOUT-1748160000000-A1B2C3D4). Generate a fresh one per operation and reuse the same one on retries.

Check payout status

Reconcile a payout by the reference you sent at initiation.

var payout = await client.Payouts.GetStatusAsync("PAYOUT-20260604-001");

switch (payout.ParsedStatus().Outcome())
{
    case PayoutOutcome.Succeeded:   /* funds delivered */            break;
    case PayoutOutcome.Reversed:    /* failed — wallet re-credited */ break;
    case PayoutOutcome.Reconciling: /* PENDING — check again later */ break;
}

Returns PayoutStatusModel.TransactionReference, .Status, .Amount, .DestinationAccountNumber, .DestinationAccountName, .DestinationBankName, .Narration, .CreatedAt. Parse .Status with .ParsedStatus()PayoutStatus.

Status Terminal Meaning
PENDING no Submitted; terminal outcome not yet recorded (reconciling).
SUCCESS yes Completed successfully.
REVERSED yes Failed/reversed — the merchant wallet was re-credited.

Collect a payment

var collection = await client.Collection.InitiateAsync(new()
{
    Amount        = "1500.00",
    Currency      = "NGN",
    Email         = "customer@example.com",
    TransactionId = WayaQuickClient.GenerateReference("TXN"),
    FirstName     = "John",
    LastName      = "Doe",
    Phone         = "08012345678",
    Description   = "Order #1234",
});
// Redirect the customer to collection.CheckOutUrl to complete payment.
// Confirm the result on your server before fulfilling the order.

Returns CollectionResponseModel.UniqueId, .TransactionId, .CheckOutUrl, .Amount, .Email, .MerchantId.

Check collection (deposit) status

The deposit webhook is the primary signal; this endpoint is the pull/safety-net path for reconciliation. Look it up by refNo (the gateway transactionId / webhook OrderId).

var deposit = await client.Collection.GetStatusAsync("1779662251460508970");

if (deposit.ParsedStatus() == CollectionStatus.Successful)
{
    // Funds confirmed — fulfil. Use deposit.RefNo as the idempotency key.
}
else if (!deposit.ParsedStatus().IsTerminal())
{
    // Still in flight — keep polling; don't refund or retry.
}

Returns CollectionStatusModel.RefNo, .TranId, .MerchantId, .Amount, .AmountPaid, .Fee, .CurrencyCode, .Status, .SettlementStatus, .Channel, .ProcessedBy, .CustomerEmail, .Description, .Environment, .TranDate. Parse .Status with .ParsedStatus()CollectionStatus.

Amount is the expected amount; AmountPaid is what was actually received — it can be smaller (PARTIAL underpayment) or larger (overpayment). Use Status + AmountPaid as authoritative.

Status Terminal Outcome Meaning
INITIATED / PENDING / PROCESSING / APPROVED no InFlight In flight — keep polling; don't refund or retry.
PARTIAL no InFlight Customer underpaid into a virtual account.
SUCCESSFUL yes Succeeded Funds confirmed — fulfil (use RefNo for idempotency).
REFUNDED yes Refunded Previously-successful transaction refunded.
FAILED / DECLINED / REJECTED / ABANDONED / EXPIRED / CANCELLED / CUSTOMER_ERROR / FRAUD_ERROR yes NotDebited Customer not debited — no fulfilment.
TIMEOUT / ERROR / SYSTEM_ERROR / BANK_ERROR yes Indeterminate Outcome unknown — reconcile, don't refund unilaterally.

A reference that doesn't belong to the authenticated merchant returns 404 (surfaced as HttpRequestException).

Process webhooks

WayaQuick POSTs your server whenever a transaction becomes SUCCESSFUL, PARTIAL, or FAILED, so you can fulfil orders in real time instead of polling. Verify every webhook before acting on itConstructEvent checks the HMAC-SHA256 signature and the replay window, and throws WayaQuickWebhookException on anything it can't trust.

The signature is computed over the exact raw request bytes. Capture the body before any JSON middleware re-serialises it, or the recomputed HMAC won't match.

using WayaQuick;
using WayaQuick.Models.Webhook;

app.MapPost("/waya/webhook", async (HttpRequest request) =>
{
    // Read the RAW body — do not let model binding touch it first.
    using var reader = new StreamReader(request.Body);
    var rawBody = await reader.ReadToEndAsync();

    WebhookEvent evt;
    try
    {
        evt = WayaQuickWebhook.ConstructEvent(
            payload:   rawBody,
            timestamp: request.Headers[WayaQuickWebhook.TimestampHeader],
            signature: request.Headers[WayaQuickWebhook.SignatureHeader],
            secret:    webhookSecret); // merchantSecretTestKey or merchantProductionSecretKey
    }
    catch (WayaQuickWebhookException)
    {
        return Results.Unauthorized(); // unsigned / forged / stale — reject
    }

    // Acknowledge fast (within ~10s), then queue the real work. OrderId is your idempotency key.
    switch (evt.ParsedStatus())
    {
        case WebhookStatus.Successful: /* upsert by evt.OrderId, then fulfil */ break;
        case WebhookStatus.Partial:    /* hold; query status by OrderId for amount paid */ break;
        case WebhookStatus.Failed:     /* no fulfilment */ break;
    }

    return Results.Ok();
});

Returns WebhookEvent (synchronous, not a Task) — .OrderId, .Amount, .Fee, .Currency, .Status, .Description, .TranTime, .TransactionDate, .ProductName, .BusinessName, .Customer (.Name, .Email, .PhoneNumber, .CustomerId), .MerchantId, .BranchCategory, .RecurrentPayment. Parse .Status with .ParsedStatus()WebhookStatus. Throws WayaQuickWebhookException instead of returning when verification fails.

Status WebhookStatus What to do
SUCCESSFUL Successful Fulfil the order. Check OrderId for idempotency.
PARTIAL Partial Hold fulfilment — query the status endpoint by OrderId for the latest amount paid.
FAILED Failed No fulfilment.

Notes:

  • The merchant secret is your merchantSecretTestKey (TEST) or merchantProductionSecretKey (PRODUCTION). Keep one verifier per environment and route by which key validates.
  • The same OrderId may fire more than once (a PARTIAL then a SUCCESSFUL, or a re-emitted SUCCESSFUL). Always upsert keyed by OrderId; never blindly insert.
  • Replay protection rejects timestamps outside a 5-minute window by default. Override via the tolerance parameter (pass Timeout.InfiniteTimeSpan to disable — not recommended).
  • Delivery is fire-and-forget: respond 200 quickly, do heavy work off-thread, and reconcile periodically with the status endpoint.

For a signature-only check (no replay window), use WayaQuickWebhook.VerifySignature(...), which returns a bool.

Via the client

If you set WebhookSecret on WayaQuickOptions, the same calls are available on the client without passing the secret each time:

var client = new WayaQuickClient(new WayaQuickOptions
{
    MerchantId    = "MER_...",
    SecretKey     = "WAYASECK_TEST_...",
    WebhookSecret = "your-merchant-webhook-secret",
});

var evt = client.Webhooks.ConstructEvent(rawBody, timestamp, signature);

client.Webhooks.ConstructEvent / VerifySignature also have overloads that take an explicit secret, so a single endpoint can route TEST vs PRODUCTION by trying each key.

BVN identity check

var identity = await client.Identity.VerifyBvnAsync(new()
{
    Bvn = "22500809037", // exactly 11 digits — validated locally before the request
});
Console.WriteLine($"{identity.FirstName} {identity.LastName}");

Returns BvnIdentityResponseModel.Bvn, .FirstName, .MiddleName, .LastName, .DateOfBirth, .Gender, .PhoneNumber1, .Email, .Nationality, .StateOfOrigin, .LgaOfOrigin, .LgaOfResidence, .ResidentialAddress, .MaritalStatus, .RegistrationDate, .WatchListed, .Base64Image.

BVN data is sensitive personal information. Store, transmit, and log it only as your data-protection obligations allow.

Error handling

Failed requests throw HttpRequestException with the API message as the exception message.

try
{
    await client.Payouts.InitiateAsync(input);
}
catch (HttpRequestException e)
{
    Console.Error.WriteLine(e.Message); // e.g. "IP 1.2.3.4 is not whitelisted"
}

Input validation errors (missing required fields, malformed BVN, missing BankCode) throw ArgumentException or ArgumentNullException before any network call is made.

Options

new WayaQuickOptions
{
    MerchantId    = "MER_...",
    SecretKey     = "WAYASECK_...",
    WebhookSecret = "...",     // optional: enables client.Webhooks without passing a secret
    TimeoutMs     = 30_000,    // default: 30 s
    MaxRetries    = 2,         // default: 2 — GET only, exponential backoff
    HttpClient    = ...,       // optional: inject your own (DI, handler chains, test fakes)
}

Retries apply to GET requests only (bank list) on timeouts, network errors, 429, and 5xx. Writes never auto-retry.

Dependency injection

services.AddSingleton(sp => new WayaQuickClient(new WayaQuickOptions
{
    MerchantId = config["WayaQuick:MerchantId"]!,
    SecretKey  = config["WayaQuick:SecretKey"]!,
    HttpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient("wayaquick"),
}));

Full example

See samples/ConsoleDemo/Program.cs for a runnable end-to-end demo covering banks, account verification, BVN, payouts, collections, status checks, and webhook verification.

WAYA_MERCHANT_ID=MER_... WAYA_SECRET_KEY=WAYASECK_TEST_... dotnet run --project samples/ConsoleDemo

Going live

On the merchant dashboard: finish KYC, grab your Merchant ID, generate your secret key under Settings → API Keys and Webhooks, and whitelist your server IPs. Swap WAYASECK_TEST_... for WAYASECK_... — the rest of your code stays the same.

Contributing

See CONTRIBUTING.md.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.1 123 7/17/2026