Solentik.Hubtel
1.0.4
dotnet add package Solentik.Hubtel --version 1.0.4
NuGet\Install-Package Solentik.Hubtel -Version 1.0.4
<PackageReference Include="Solentik.Hubtel" Version="1.0.4" />
<PackageVersion Include="Solentik.Hubtel" Version="1.0.4" />
<PackageReference Include="Solentik.Hubtel" />
paket add Solentik.Hubtel --version 1.0.4
#r "nuget: Solentik.Hubtel, 1.0.4"
#:package Solentik.Hubtel@1.0.4
#addin nuget:?package=Solentik.Hubtel&version=1.0.4
#tool nuget:?package=Solentik.Hubtel&version=1.0.4
Solentik.Hubtel
Strongly typed Hubtel hosted payments, invoicing, transfers, and account verification for .NET 8, 9, and 10.
Documentation
Full installation, configuration, and API documentation is available at:
https://hubtel.solentik.com/dotnet/introduction.html
Install
dotnet add package Solentik.Hubtel --version 1.2.0
Register only what you use
using Solentik.Hubtel.DependencyInjection;
builder.Services.AddHubtelPayments(builder.Configuration);
builder.Services.AddHubtelInvoicing(builder.Configuration);
builder.Services.AddHubtelTransfers(builder.Configuration);
builder.Services.AddHubtelVerification(builder.Configuration);
AddHubtelPayments, AddHubtelInvoicing, AddHubtelTransfers, and AddHubtelVerification are all independent. An application that only sends invoices does not need transfer credentials, and vice versa.
Configuration
Both registration methods bind configuration from the standard .NET configuration pipeline, including appsettings.json, environment-specific settings, user secrets, and environment variables.
Every Hubtel feature revolves around the same two account numbers for a given merchant: the Collection account (used by Payments, Invoicing, Verification, and the Collection side of Transfers) and the Disbursement account (used by the Transfers payout side). Set them once at the top of the Hubtel section and every feature picks them up automatically:
{
"Hubtel": {
"CollectionAccountNumber": "your-collection-account-number",
"DisbursementAccountNumber": "your-disbursement-account-number",
"Payments": {
"ApiKey": "your-application-key",
"ApiSecret": "your-application-secret",
"CallbackUrl": "https://api.example.com/hubtel/payments/callback",
"ReturnUrl": "https://example.com/payment-complete",
"CancellationUrl": "https://example.com/payment-cancelled",
"Timeout": "00:00:45"
},
"Invoicing": {
"ApiId": "your-invoicing-api-id",
"ApiKey": "your-invoicing-api-key",
"CallbackUrl": "https://api.example.com/hubtel/invoices/callback",
"BaseAddress": "https://invoicing.hubtel.com/",
"Timeout": "00:00:45"
},
"Transfers": {
"ApiKey": "your-disbursement-api-key",
"ApiSecret": "your-disbursement-api-secret",
"Timeout": "00:00:45"
},
"Verification": {
"ApiKey": "your-verification-api-key",
"ApiSecret": "your-verification-api-secret",
"Timeout": "00:00:45"
}
}
}
Each feature still has its own CollectionAccountNumber (and, for Transfers, DisbursementAccountNumber) property; setting one explicitly under Hubtel:Payments, Hubtel:Invoicing, Hubtel:Transfers, or Hubtel:Verification overrides the shared value for that feature only, for the rare merchant whose account numbers genuinely differ per product. Each feature keeps its own API credentials regardless, since Hubtel issues separate keys per product.
::: tip Migrating from an older config
Hubtel:Payments:MerchantAccountNumber still works; it is a deprecated alias bound to the same value as CollectionAccountNumber and can be renamed at your convenience. Existing per-feature CollectionAccountNumber/DisbursementAccountNumber values under Invoicing, Transfers, and Verification also keep working unchanged; nothing breaks if you do not adopt the shared top-level fields.
:::
The default API endpoints are built into the package. They can be overridden through InitiateEndpoint, TransactionStatusEndpointTemplate, or BaseAddress when required.
Environment variables
.NET maps double underscores to nested configuration sections:
Hubtel__CollectionAccountNumber=your-collection-account-number
Hubtel__DisbursementAccountNumber=your-disbursement-account-number
Hubtel__Payments__ApiKey=your-application-key
Hubtel__Payments__ApiSecret=your-application-secret
Hubtel__Payments__CallbackUrl=https://api.example.com/hubtel/payments/callback
Hubtel__Payments__ReturnUrl=https://example.com/payment-complete
Hubtel__Payments__CancellationUrl=https://example.com/payment-cancelled
Hubtel__Invoicing__ApiId=your-invoicing-api-id
Hubtel__Invoicing__ApiKey=your-invoicing-api-key
Hubtel__Invoicing__CallbackUrl=https://api.example.com/hubtel/invoices/callback
User secrets
For local development:
dotnet user-secrets set "Hubtel:CollectionAccountNumber" "your-collection-account-number"
dotnet user-secrets set "Hubtel:DisbursementAccountNumber" "your-disbursement-account-number"
dotnet user-secrets set "Hubtel:Payments:ApiKey" "your-application-key"
dotnet user-secrets set "Hubtel:Payments:ApiSecret" "your-application-secret"
dotnet user-secrets set "Hubtel:Invoicing:ApiId" "your-invoicing-api-id"
dotnet user-secrets set "Hubtel:Invoicing:ApiKey" "your-invoicing-api-key"
Do not commit production credentials to appsettings.json. Use environment variables, user secrets, or your deployment platform's secret store.
Hosted payment
var response = await hubtel.Payments.InitiateAsync(
new CreatePaymentRequest
{
TotalAmount = 100m,
Description = "Order ORD-123",
ClientReference = "ORD-123-12434"
},
cancellationToken);
var checkoutUrl = response.Data?.CheckoutUrl;
Invoice
var response = await invoices.CreateAsync(
new CreateInvoiceRequest
{
InvoiceNumber = "INV-123",
DueDate = DateTimeOffset.UtcNow.AddDays(7),
CreatedBy = "Billing",
CustomerName = "John Doe",
CustomerPhoneNumber = "233555654321",
Items =
[
new InvoiceItem { Description = "Service", Quantity = 1, UnitPrice = 100m }
]
},
cancellationToken);
The invoicing client supports one-time invoices through CreateAsync and repeat invoices through CreateRepeatAsync. Both hosted payments and invoices provide transaction-status checks for callback recovery and reconciliation. Callback parsers are registered as IHubtelCallbackParser and IInvoiceCallbackParser.
Transfers
There is no sandbox for any transfer endpoint. Every call below moves real money or queries a real account, in production, the moment it's called.
var result = await transfers.SendMoneyAsync(
new SendMoneyRequest
{
RecipientName = "Jane Doe",
RecipientMsisdn = "233249111411",
Channel = "mtn-gh",
Amount = 150m,
PrimaryCallbackUrl = new Uri("https://api.example.com/webhooks/hubtel-send-money"),
Description = "October payroll",
ClientReference = "PAY-2026-10-001"
},
cancellationToken);
if (result.Success) { /* transferred */ }
else if (result.Pending) { /* not yet final; wait for the callback, or poll GetSendStatusAsync */ }
else { /* failed; result.Message has Hubtel's reason */ }
ITransferClient also provides SendToBankAsync, GetSendStatusAsync (the mandatory fallback for a missed callback; requires your outbound IP to be whitelisted with Hubtel), GetCollectionBalanceAsync/GetDisbursementBalanceAsync, TransferToDisbursementAsync (moves funds from Collection into Disbursement so Send Money/Send-to-Bank has funds to pay out from), and GetBalanceTransferStatusAsync.
ClientReference is not a retry key. Every mutating call (SendMoneyAsync, SendToBankAsync, TransferToDisbursementAsync) takes a ClientReference (max 36 characters) that Hubtel treats as a permanent, never-reusable value across this account, for all time. Resubmitting the same reference after a timeout does not safely retry the operation; Hubtel rejects it outright. Generate a fresh reference for every attempt and use GetSendStatusAsync/GetBalanceTransferStatusAsync to find out what actually happened to a request whose outcome is unknown.
Status-check and balance-query methods (GetSendStatusAsync, GetCollectionBalanceAsync, GetDisbursementBalanceAsync, GetBalanceTransferStatusAsync) return null instead of throwing when Hubtel can't be reached or returns a failure, so a recovery job or balance display can degrade gracefully.
SendMoneyResult/TransferResult model Hubtel's three-state outcome rather than a bare bool: Success, Pending (Hubtel accepted the request but hasn't finalized it; the real outcome arrives via the callback or GetSendStatusAsync), or neither (failed, with Message carrying Hubtel's reason). Treat Pending as a distinct state, not as success or failure.
Send Money/Send-to-Bank and Balance Transfer callbacks are parsed with ITransferCallbackParser and IBalanceTransferCallbackParser, registered alongside AddHubtelTransfers.
Verification
Verify a payout account before sending money to it:
var result = await verification.VerifyBankAccountAsync(bankCode: "300304", accountNumber: "1234567890", cancellationToken);
if (result.Success)
{
// result.AccountName is the registered account holder
}
else
{
// result.Message may still be a useful, user-facing reason even on failure
}
IVerificationClient also provides VerifyMobileMoneyAsync(channel, customerMsisdn). Hubtel does not publish a "list bank/momo channels" API, so bank/network codes (e.g. "300304", "mtn-gh") are application-managed data you maintain yourself from Hubtel's own documentation.
A false result can still carry a genuinely useful Message; Hubtel sometimes reports a soft failure (e.g. "we couldn't fully verify this, save it anyway?") that's worth showing to whoever is entering the payout account, rather than replacing with a generic error.
Securing your callback endpoint
IHubtelCallbackParser/IInvoiceCallbackParser/ITransferCallbackParser/IBalanceTransferCallbackParser only parse and validate the shape of a callback body; they do not verify that a request actually came from Hubtel. Hubtel has no webhook signature scheme for any of its APIs. Anyone who can reach your callback URL can POST a fabricated "payment succeeded" (or "transfer succeeded") body. Before crediting an order/invoice or treating a transfer as complete:
- Restrict the endpoint (a random/secret path segment, an IP allowlist for Hubtel's published ranges, or mutual TLS if your deployment supports it).
- Treat the callback as a hint, not a source of truth: call
Transactions.GetStatusAsync(orInvoices.GetStatusAsync,Transfers.GetSendStatusAsync,Transfers.GetBalanceTransferStatusAsync) to re-confirm the outcome directly with Hubtel before acting on it.
Resilience and observability
AddHubtelPayments, AddHubtelInvoicing, AddHubtelTransfers, and AddHubtelVerification all wire their HttpClient through AddStandardResilienceHandler, which adds retry, circuit-breaker, and timeout handling for transient failures (network errors, timeouts, and 408/429/5xx responses). Retries are deliberately disabled for POST requests; payment initiation, invoice creation, Send Money, Send-to-Bank, and balance transfers are not safe to replay automatically, since a retried request after a timeout could double-charge, double-invoice, or double-pay a real person if the original request actually reached Hubtel. GET requests (status checks, balance queries, verification) are naturally idempotent and are retried.
If an ILoggerFactory is registered in your service collection, each client logs a Debug entry for successful requests and a Warning entry (method, URI, status code, Hubtel response code, and message; never credentials) for failed ones.
See the hubtel-docs site in the repository for installation, configuration, payment, invoicing, status-check, callback, and error-handling guides.
| 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 is compatible. 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
- Microsoft.Extensions.Http (>= 10.0.11)
- Microsoft.Extensions.Http.Resilience (>= 10.9.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.11)
-
net8.0
- Microsoft.Extensions.Http (>= 10.0.11)
- Microsoft.Extensions.Http.Resilience (>= 10.9.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.11)
-
net9.0
- Microsoft.Extensions.Http (>= 10.0.11)
- Microsoft.Extensions.Http.Resilience (>= 10.9.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.