Paybox.AspNetCore
0.1.0
dotnet add package Paybox.AspNetCore --version 0.1.0
NuGet\Install-Package Paybox.AspNetCore -Version 0.1.0
<PackageReference Include="Paybox.AspNetCore" Version="0.1.0" />
<PackageVersion Include="Paybox.AspNetCore" Version="0.1.0" />
<PackageReference Include="Paybox.AspNetCore" />
paket add Paybox.AspNetCore --version 0.1.0
#r "nuget: Paybox.AspNetCore, 0.1.0"
#:package Paybox.AspNetCore@0.1.0
#addin nuget:?package=Paybox.AspNetCore&version=0.1.0
#tool nuget:?package=Paybox.AspNetCore&version=0.1.0
Paybox
A modular .NET 10 payment gateway library. Install only the providers you need, plug in your own persistence layer, and get webhook endpoints wired up in two lines.
Currently supported: PayMe (payme.uz) · Click (click.uz)
Planned: Stripe · Paymob · Uzum Bank
Packages
| Package | Description |
|---|---|
Paybox.Abstractions |
Interfaces and models only — no dependencies |
Paybox.Core |
DI builder, PaymentService, signature utilities |
Paybox.PayMe |
PayMe Merchant API + webhook handler |
Paybox.Click |
Click Shop API + Merchant API + webhook handler |
Paybox.AspNetCore |
MapPayboxWebhooks() minimal API routing |
Installation
dotnet add package Paybox.Core
dotnet add package Paybox.PayMe
dotnet add package Paybox.Click
dotnet add package Paybox.AspNetCore
Quick start
1. Register services
// Program.cs
builder.Services.AddPaybox()
.AddPayMe(o =>
{
o.MerchantId = builder.Configuration["PayMe:MerchantId"]!;
o.SecretKey = builder.Configuration["PayMe:SecretKey"]!;
o.IsSandbox = builder.Environment.IsDevelopment();
})
.AddClick(o =>
{
o.ServiceId = int.Parse(builder.Configuration["Click:ServiceId"]!);
o.SecretKey = builder.Configuration["Click:SecretKey"]!;
o.MerchantUserId = int.Parse(builder.Configuration["Click:MerchantUserId"]!);
o.IsSandbox = builder.Environment.IsDevelopment();
});
// required — implement with your own DB/ORM (see below)
builder.Services.AddScoped<IPaymentEventStore, MyPaymentEventStore>();
// optional — side effects after payment lifecycle transitions
builder.Services.AddScoped<IPaymentLifecycleHook, MyNotificationHook>();
2. Map webhook endpoints
app.MapPayboxWebhooks(); // uses prefix "/payments" by default
This registers:
| Method | Route | Description |
|---|---|---|
| POST | /payments/payme/callback |
PayMe JSON-RPC callback |
| POST | /payments/click/prepare |
Click prepare phase (action=0) |
| POST | /payments/click/complete |
Click complete phase (action=1) |
Custom prefix:
app.MapPayboxWebhooks(prefix: "/api/v1/payments");
3. Initiate a payment
public class OrderService(IPaymentService payments)
{
public async Task<string> CreatePaymentAsync(string orderId, long amountTiyin)
{
var request = new PaymentRequest(
OrderId: orderId,
Amount: amountTiyin, // always in smallest unit (tiyin for UZS)
Currency: PaymentCurrency.UZS,
ReturnUrl: "https://yoursite.com/orders/thank-you"
);
var result = await payments.InitiateAsync("PayMe", request);
// result.CheckoutUrl → redirect the user here
return result.CheckoutUrl!;
}
}
Implement IPaymentEventStore
Paybox has no opinion on your database. Implement this interface with whatever ORM or driver you use:
public sealed class PaymentEventStore(AppDbContext db) : IPaymentEventStore
{
public async Task SaveAsync(PaymentEvent @event, CancellationToken ct = default)
{
db.PaymentEvents.Add(new PaymentEventEntity
{
Id = @event.Id,
OrderId = @event.OrderId,
TransactionId = @event.TransactionId,
Provider = @event.Provider,
Amount = @event.Amount,
Currency = @event.Currency.ToString(),
Status = @event.Status.ToString(),
CreatedAt = @event.CreatedAt,
UpdatedAt = @event.UpdatedAt
});
await db.SaveChangesAsync(ct);
}
public async Task<PaymentEvent?> FindByTransactionIdAsync(string transactionId, CancellationToken ct = default)
{
var entity = await db.PaymentEvents
.FirstOrDefaultAsync(e => e.TransactionId == transactionId, ct);
return entity is null ? null : Map(entity);
}
public async Task<PaymentEvent?> FindByOrderIdAsync(string orderId, CancellationToken ct = default)
{
var entity = await db.PaymentEvents
.FirstOrDefaultAsync(e => e.OrderId == orderId, ct);
return entity is null ? null : Map(entity);
}
public async Task UpdateStatusAsync(string transactionId, PaymentStatus status, CancellationToken ct = default)
{
await db.PaymentEvents
.Where(e => e.TransactionId == transactionId)
.ExecuteUpdateAsync(s => s
.SetProperty(e => e.Status, status.ToString())
.SetProperty(e => e.UpdatedAt, DateTimeOffset.UtcNow), ct);
}
private static PaymentEvent Map(PaymentEventEntity e) => new()
{
Id = e.Id,
OrderId = e.OrderId,
TransactionId = e.TransactionId,
Provider = e.Provider,
Amount = e.Amount,
Currency = Enum.Parse<PaymentCurrency>(e.Currency),
Status = Enum.Parse<PaymentStatus>(e.Status),
CreatedAt = e.CreatedAt,
UpdatedAt = e.UpdatedAt
};
}
Implement IPaymentLifecycleHook (optional)
Fires after the event is persisted. Use it for order updates, email/SMS notifications, etc.
public sealed class OrderNotificationHook(IOrderService orders, IEmailService email) : IPaymentLifecycleHook
{
public async Task OnCompletedAsync(PaymentEvent @event, CancellationToken ct = default)
{
await orders.MarkPaidAsync(@event.OrderId, ct);
await email.SendPaymentConfirmationAsync(@event.OrderId, ct);
}
public async Task OnCancelledAsync(PaymentEvent @event, CancellationToken ct = default)
{
await orders.MarkCancelledAsync(@event.OrderId, ct);
}
}
Configuration reference
appsettings.json
{
"PayMe": {
"MerchantId": "your_merchant_id",
"SecretKey": "your_secret_key",
"IsSandbox": true
},
"Click": {
"ServiceId": 12345,
"SecretKey": "your_secret_key",
"MerchantUserId": 99,
"IsSandbox": false
}
}
Bind from config directly:
builder.Services.AddPaybox()
.AddPayMe(o => builder.Configuration.GetSection("PayMe").Bind(o))
.AddClick(o => builder.Configuration.GetSection("Click").Bind(o));
Payment flow
PayMe
User → Your server (InitiateAsync) → checkout URL → User redirected to PayMe
PayMe → POST /payments/payme/callback (JSON-RPC: CheckPerform → Create → Perform)
Paybox → IPaymentEventStore.UpdateStatusAsync → IPaymentLifecycleHook.OnCompletedAsync
Amount: tiyin (1 UZS = 100 tiyin). Pass 50000 for 500 UZS.
Click
User → Your server (InitiateAsync) → checkout URL → User redirected to Click
Click → POST /payments/click/prepare (action=0, validates order)
Click → POST /payments/click/complete (action=1, confirms payment)
Paybox → IPaymentEventStore.UpdateStatusAsync → IPaymentLifecycleHook.OnCompletedAsync
Amount: convert to som internally. Pass 50000 tiyin — Paybox converts to 500.00 som for Click.
Check payment status
var status = await payments.GetStatusAsync("PayMe", transactionId);
// status.Status → PaymentStatus.Completed / Pending / Cancelled / Failed
Refund
var refund = await payments.RefundAsync("Click", transactionId, amount: 50000);
Click allows refunds only for same-month transactions (except the 1st day of the month).
PayMe IP whitelist
PayMe callbacks come from 185.234.113.1–185.234.113.15. The handler rejects requests outside this range by default. Override for testing:
.AddPayMe(o =>
{
// ...
o.AllowedIps = ["185.234.113.", "10.0.0."]; // custom ranges
});
Adding a new provider
- Create a class library referencing
Paybox.Core - Implement
IPaymentProviderandIWebhookHandler - Add a
AddMyProvider(this PayboxBuilder builder, ...)extension method - Register with
builder.Services.TryAddEnumerable
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- Paybox.Core (>= 0.1.0)
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 |
|---|---|---|
| 0.1.0 | 106 | 4/18/2026 |