Paystack.SDK
1.1.1
dotnet add package Paystack.SDK --version 1.1.1
NuGet\Install-Package Paystack.SDK -Version 1.1.1
<PackageReference Include="Paystack.SDK" Version="1.1.1" />
<PackageVersion Include="Paystack.SDK" Version="1.1.1" />
<PackageReference Include="Paystack.SDK" />
paket add Paystack.SDK --version 1.1.1
#r "nuget: Paystack.SDK, 1.1.1"
#:package Paystack.SDK@1.1.1
#addin nuget:?package=Paystack.SDK&version=1.1.1
#tool nuget:?package=Paystack.SDK&version=1.1.1
Paystack.SDK
A .NET class library for the Paystack API with a result-based error model, automatic amount-to-subunit conversion, built-in fee absorption, and ready-made ASP.NET Core DI registration. Covers the Transaction, Charge (direct mobile money/OTP/PIN), Settlement, and Subaccount resources. Targets .NET 8 and .NET 10.
Installation
dotnet add package Paystack.SDK
Configuration
Add your Paystack secret key to appsettings.json:
"Paystack": {
"SecretKey": "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
Get your secret key from the Paystack dashboard. Use
sk_test_...during development.
Registration
// Program.cs
builder.Services.AddPaystackSdk(config =>
{
config.SecretKey = builder.Configuration["Paystack:SecretKey"]!;
});
With a custom base URL (rarely needed — defaults to https://api.paystack.co):
builder.Services.AddPaystackSdk(config =>
{
config.SecretKey = "sk_live_...";
config.BaseUrl = "https://api.paystack.co";
});
This registers the following as scoped services:
IPaystackTransactionServiceIPaystackChargeServiceIPaystackSettlementServiceIPaystackSubaccountService
Usage
Every method returns a PaystackResult<T> — no exceptions are thrown for API errors. Check IsSuccess before using Data.
Initialize a transaction (redirect checkout)
public class CheckoutService(IPaystackTransactionService transactions)
{
public async Task<string?> StartCheckoutAsync(string email, decimal amount)
{
var result = await transactions.InitializeAsync(new InitializeTransactionRequest(
Email: email,
Amount: amount,
CallbackUrl: "https://myapp.com/payment/callback"
));
if (!result.IsSuccess)
{
// result.Message contains the Paystack error message
return null;
}
// Redirect the user here to complete payment
return result.Data!.AuthorizationUrl;
}
}
Verify a transaction after callback
using Paystack.SDK.Transactions.Extensions;
var result = await transactions.VerifyAsync(reference);
if (result.IsVerifiedSuccess())
{
// Payment confirmed — fulfill the order
}
IsVerifiedSuccess() combines the API-level and transaction-level checks: the call must have succeeded and Paystack must report the transaction as success. A parallel IsChargeSuccess() exists in Paystack.SDK.Charge.Extensions for mobile money / direct charges.
Charge mobile money directly (Ghana)
public class MobileMoneyService(IPaystackChargeService charge)
{
public async Task PayWithMomoAsync(string email, decimal amount, string phone)
{
var result = await charge.ChargeMobileMoneyAsync(new MobileMoneyChargeRequest(
Email: email,
Amount: amount,
Phone: phone,
Network: charge.GetNetworkProvider(phone)! // auto-detect mtn | vod | atl
));
if (!result.IsSuccess) return;
var status = result.Data!.Data?.Status;
// status = "send_otp" → prompt user, call SubmitOtpAsync
// status = "send_birthday" → prompt user, call SubmitBirthdayAsync
// status = "pending" → call PollUntilCompleteAsync
// status = "success" → charge completed
}
}
List transactions with pagination
var result = await transactions.ListAsync(new ListTransactionsRequest(
PerPage: 50,
Page: 1,
Status: "success"
));
if (result.IsSuccess)
{
foreach (var tx in result.Data!.Items) { /* ... */ }
var meta = result.Data.Meta;
// meta.Total, meta.Page, meta.PerPage, meta.PageCount
}
Poll until a pending charge completes
var final = await charge.PollUntilCompleteAsync(
reference: "abc123",
maxAttempts: 20,
intervalSeconds: 5,
ct: cancellationToken
);
Fee absorption
Customers can absorb the Paystack processing fee so the merchant receives exactly the intended amount. Use PaystackFeeCalculator to show the fee up-front, or set AbsorbFee: true on any request to apply the gross-up automatically.
Show the fee before checkout
decimal fee = PaystackFeeCalculator.FeeOnly(100m);
// fee → 1.99 (1.95% of 101.99)
var breakdown = PaystackFeeCalculator.GrossUp(100m);
// breakdown.OriginalAmount → 100.00 (merchant receives)
// breakdown.FeeAmount → 1.99 (customer absorbs)
// breakdown.TotalCharged → 101.99 (passed to Paystack)
Apply the fee automatically
await transactions.InitializeAsync(new InitializeTransactionRequest(
Email: email,
Amount: 100m,
AbsorbFee: true // Paystack gets 101.99, merchant receives 100
));
Custom fee rate (e.g. international cards at 3.9%)
await transactions.InitializeAsync(new InitializeTransactionRequest(
Email: email,
Amount: 100m,
AbsorbFee: true,
FeeRate: 0.039m
));
The AbsorbFee flag is available on InitializeTransactionRequest, ChargeAuthorizationRequest, and MobileMoneyChargeRequest.
Transaction splits
Paystack supports two ways to split a single payment between your main account and a subaccount. Which one runs is decided by which fields you send on InitializeAsync / ChargeAuthorizationAsync — there is no separate endpoint.
Mode 1 — Flat split (pre-configured, reusable)
Create a split once via Paystack's dashboard or /split endpoint. You get back a split_code like SPL_xxx. On every transaction just pass the code:
await transactions.InitializeAsync(new InitializeTransactionRequest(
Email: email,
Amount: 100m,
SplitCode: "SPL_xxx" // Paystack applies the saved rules
));
Bearer and TransactionCharge are ignored when SplitCode is set.
Mode 2 — Dynamic split (per-transaction)
No pre-created split. Define the split inline with the Subaccount of the party to pay, optional Bearer (who pays the Paystack fee), and optional TransactionCharge (a flat fee the subaccount owes on this transaction):
await transactions.InitializeAsync(new InitializeTransactionRequest(
Email: email,
Amount: 100m,
Subaccount: "ACCT_xxx", // party to split to
Bearer: "subaccount", // "account" (you) or "subaccount" — optional
TransactionCharge: 2.00m // major units (GHS/NGN), converted to subunits — optional
));
Paystack picks the mode by presence of fields: SplitCode → Mode 1 · Subaccount without SplitCode → Mode 2 · neither → normal transaction.
The same four fields — SplitCode, Subaccount, Bearer, TransactionCharge — are also accepted on ChargeAuthorizationRequest.
Webhook verification
Paystack signs every webhook body with HMAC-SHA512 using your secret key and sends the hex digest in the x-paystack-signature header. Use PaystackWebhook.IsValid to verify it before trusting the payload.
using Paystack.SDK.Webhooks;
app.MapPost("/webhooks/paystack", async (HttpRequest req, IConfiguration config) =>
{
using var reader = new StreamReader(req.Body);
var rawBody = await reader.ReadToEndAsync();
var signature = req.Headers[PaystackWebhook.SignatureHeader].ToString();
var secretKey = config["Paystack:SecretKey"]!;
if (!PaystackWebhook.IsValid(rawBody, signature, secretKey))
return Results.Unauthorized();
// Signature valid — safe to deserialize and handle the event
return Results.Ok();
});
Important: verify against the raw request body bytes. Any re-serialization, reformatting, or model-binding before verification will break the signature.
API Reference
IPaystackTransactionService
| Method | Description |
|---|---|
InitializeAsync(request, ct) |
Starts a redirect-based checkout and returns an AuthorizationUrl |
VerifyAsync(reference, ct) |
Verifies a transaction by reference |
ListAsync(request?, ct) |
Lists transactions with optional filters. Returns a PagedResult<TransactionData> exposing Items and Meta (total, page, perPage, pageCount) |
FetchAsync(transactionId, ct) |
Fetches a single transaction by ID |
ChargeAuthorizationAsync(request, ct) |
Charges a saved authorization code (recurring payment) |
TotalsAsync(ct) |
Returns aggregate transaction totals |
result.IsVerifiedSuccess() (extension) |
One-liner: true if the call succeeded AND the transaction status is success |
IPaystackChargeService
| Method | Description |
|---|---|
ChargeMobileMoneyAsync(request, ct) |
Charges a Ghana mobile money account |
SubmitOtpAsync(reference, otp, ct) |
Submits OTP when status is send_otp |
SubmitPhoneAsync(reference, phone, ct) |
Submits phone when status is send_phone |
SubmitBirthdayAsync(reference, birthday, ct) |
Submits birthday (yyyy-MM-dd) when status is send_birthday |
SubmitPinAsync(reference, pin, ct) |
Submits PIN when status is send_pin |
CheckPendingAsync(reference, ct) |
Checks the current status of a charge |
PollUntilCompleteAsync(reference, maxAttempts, intervalSeconds, ct) |
Polls until status is success, failed, or cancelled |
result.IsChargeSuccess() (extension) |
One-liner: true if the call succeeded AND the charge status is success |
DetectMobileNetwork(phone) |
Returns GhanaMobileNetwork enum from a Ghanaian phone number |
GetNetworkProvider(phone) |
Returns "mtn" \| "vod" \| "atl" or null if undetectable |
IPaystackSettlementService
| Method | Description |
|---|---|
ListAsync(request?, ct) |
Lists settlements with optional filters (page, perPage, date range, status, subaccount). Returns PagedResult<SettlementData> |
ListTransactionsAsync(settlementId, page?, perPage?, ct) |
Lists the transactions included in a specific settlement |
IPaystackSubaccountService
| Method | Description |
|---|---|
CreateAsync(request, ct) |
Creates a subaccount. Requires BusinessName, SettlementBank, AccountNumber, PercentageCharge (0–100) |
ListAsync(request?, ct) |
Lists subaccounts with optional pagination/date filters. Returns PagedResult<SubaccountData> |
FetchAsync(idOrCode, ct) |
Fetches a single subaccount by numeric id or code (e.g. ACCT_xxx) |
UpdateAsync(idOrCode, request, ct) |
Updates a subaccount. All fields optional — only non-null values are sent |
PaystackWebhook
| Member | Description |
|---|---|
IsValid(rawBody, signature, secretKey) |
Returns true if the signature is a valid HMAC-SHA512 of the raw body. Constant-time comparison. |
SignatureHeader |
Constant: "x-paystack-signature" |
PaystackFeeCalculator
| Method | Description |
|---|---|
GrossUp(amount, feeRate?) |
Returns a FeeBreakdown with the gross-up calculation |
FeeOnly(amount, feeRate?) |
Returns just the fee amount to display to the customer |
DefaultFeeRate |
Constant: 0.0195m (1.95% — Paystack Ghana local rate) |
PaystackResult<T>
| Property | Description |
|---|---|
IsSuccess |
true if the API call succeeded AND Paystack returned status: true |
Message |
Error message on failure, optional status message on success |
Data |
The deserialized response payload (null on failure) |
StatusCode |
HTTP status code from Paystack. null when the failure occurred before a response was received (e.g. DNS/connection errors). Use this to distinguish 401 (bad key) from 400 (bad request) from 5xx |
PagedResult<T>
| Property | Description |
|---|---|
Items |
The page of results (IReadOnlyList<T>) |
Meta |
PaystackMeta — Total, Skipped, PerPage, Page, PageCount |
PaystackConfig
| Property | Type | Default | Description |
|---|---|---|---|
SecretKey |
string |
— (required) | Your Paystack secret key |
BaseUrl |
string |
"https://api.paystack.co" |
API base URL |
Timeout |
TimeSpan |
100s |
HTTP request timeout. Paystack's list/totals endpoints can be slow under load — raise this if you see timeouts |
Defaults & behaviour
| Setting | Value |
|---|---|
| Base URL | https://api.paystack.co |
| HTTP client | Flurl.Http |
| Serialization | System.Text.Json with snake_case naming |
| Amount conversion | decimal → smallest unit (kobo/pesewas) applied automatically |
| Error handling | PaystackResult<T> — no exceptions for API errors |
| Default fee rate | 1.95% (Paystack Ghana local cards + mobile money) |
| Cancellation | All async methods accept CancellationToken |
Supported networks (Ghana mobile money)
| Code | Network | Prefixes |
|---|---|---|
mtn |
MTN | 024, 054, 055, 059, 053 |
vod |
Vodafone | 020, 050, 023, 028 |
atl |
AirtelTigo | 027, 057, 026, 056 |
Use IPaystackChargeService.DetectMobileNetwork(phone) to auto-detect from a phone number (with or without +233 country code).
Requirements
- .NET 8 or .NET 10
- A Paystack account with API keys enabled
| 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 is compatible. 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. |
-
net10.0
- Flurl.Http (>= 4.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
-
net8.0
- Flurl.Http (>= 4.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.1.1 — README polish to reflect .NET 8 + .NET 10 multi-target. No code changes from 1.1.0.