WayaQuick.Integration
2.0.1
dotnet add package WayaQuick.Integration --version 2.0.1
NuGet\Install-Package WayaQuick.Integration -Version 2.0.1
<PackageReference Include="WayaQuick.Integration" Version="2.0.1" />
<PackageVersion Include="WayaQuick.Integration" Version="2.0.1" />
<PackageReference Include="WayaQuick.Integration" />
paket add WayaQuick.Integration --version 2.0.1
#r "nuget: WayaQuick.Integration, 2.0.1"
#:package WayaQuick.Integration@2.0.1
#addin nuget:?package=WayaQuick.Integration&version=2.0.1
#tool nuget:?package=WayaQuick.Integration&version=2.0.1
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 —
--sourceis 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.nupkgURL (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:
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
Click Download raw file (or
curlthe 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.nupkgInstall from the folder you unzipped into (the
--sourceis 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 it — ConstructEvent 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) ormerchantProductionSecretKey(PRODUCTION). Keep one verifier per environment and route by which key validates. - The same
OrderIdmay fire more than once (aPARTIALthen aSUCCESSFUL, or a re-emittedSUCCESSFUL). Always upsert keyed byOrderId; never blindly insert. - Replay protection rejects timestamps outside a 5-minute window by default. Override via the
toleranceparameter (passTimeout.InfiniteTimeSpanto disable — not recommended). - Delivery is fire-and-forget: respond
200quickly, 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 | Versions 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. |
-
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 |