BakhodirovDev.Click.AspNetCore
0.1.0
dotnet add package BakhodirovDev.Click.AspNetCore --version 0.1.0
NuGet\Install-Package BakhodirovDev.Click.AspNetCore -Version 0.1.0
<PackageReference Include="BakhodirovDev.Click.AspNetCore" Version="0.1.0" />
<PackageVersion Include="BakhodirovDev.Click.AspNetCore" Version="0.1.0" />
<PackageReference Include="BakhodirovDev.Click.AspNetCore" />
paket add BakhodirovDev.Click.AspNetCore --version 0.1.0
#r "nuget: BakhodirovDev.Click.AspNetCore, 0.1.0"
#:package BakhodirovDev.Click.AspNetCore@0.1.0
#addin nuget:?package=BakhodirovDev.Click.AspNetCore&version=0.1.0
#tool nuget:?package=BakhodirovDev.Click.AspNetCore&version=0.1.0
Click .NET SDK
.NET SDK for Click payments (Uzbekistan). Targets .NET 10 and .NET 8.
Two packages:
| Package | What it gives you |
|---|---|
BakhodirovDev.Click.Sdk |
Signing, Shop API processor, payment-link builder, Merchant API client. Framework-agnostic. |
BakhodirovDev.Click.AspNetCore |
AddClick(...) DI + MapClickShopApi() endpoint — plug the callback into any ASP.NET Core app. |
Covers: Shop API (Prepare/Complete callbacks), Advanced Shop API (billing), payment links, Merchant API (invoices, payment status, reversal, card tokens), CLICK Pass and fiscalization.
Install
dotnet add package BakhodirovDev.Click.AspNetCore # pulls in Click.Sdk
1. Configure
appsettings.json:
{
"Click": {
"ServiceId": 12345,
"MerchantId": 6789,
"SecretKey": "your-secret-key",
"MerchantUserId": 1011
}
}
Program.cs:
builder.Services.AddClick<OrderClickHandler>(builder.Configuration);
var app = builder.Build();
app.MapClickShopApi(); // POST /click/shop — set this as both Prepare and Complete URL in the Click cabinet
app.Run();
2. Implement your business logic
This is the only class you write. The SDK already verified the md5 signature and the service_id
before your handler runs — so if OnPrepareAsync is called, the request is authentic. You only
answer domain questions.
What the SDK does vs. what you do
| The SDK does | You do |
|---|---|
Verify md5 signature (-1 on failure) |
Look the order up in your DB |
| Parse the form / JSON | Check the amount matches (-2) |
Validate service_id (-8) |
Mark the order paid, deliver the goods |
| Build the correct JSON response | Idempotency (protect against repeats) |
The two-step lifecycle
user pays on Click
│
▼
1) Prepare (action=0) ──► "Order #42 exists? amount ok? not paid?"
│ you reserve it, return merchant_prepare_id
▼
Click charges the card
│
▼
2) Complete (action=1) ──► error=0 : money taken → mark paid, deliver
error<0 : failed/cancel → release, return -9
Your domain models (yours, not the SDK's)
public class Order
{
public string Id { get; set; } = ""; // this is merchant_trans_id
public decimal Amount { get; set; }
public bool IsPaid { get; set; }
}
public enum PaymentState { Preparing, Paid, Cancelled }
public class Payment
{
public long Id { get; set; } // this becomes merchant_prepare_id
public string OrderId { get; set; } = "";
public long ClickTransId { get; set; }
public long ClickPaydocId { get; set; }
public decimal Amount { get; set; }
public PaymentState State { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Payment> Payments => Set<Payment>();
public AppDbContext(DbContextOptions<AppDbContext> o) : base(o) { }
}
The handler (EF Core example)
using Click.Sdk.Shop;
using Microsoft.EntityFrameworkCore;
public sealed class OrderClickHandler(AppDbContext db, ILogger<OrderClickHandler> log) : IClickShopHandler
{
// STEP 1 — Click asks: is this order payable? Reserve it.
public async Task<ClickPrepareResult> OnPrepareAsync(ClickShopRequest r, CancellationToken ct)
{
var order = await db.Orders.FirstOrDefaultAsync(o => o.Id == r.MerchantTransId, ct);
if (order is null) return ClickPrepareResult.Fail(ClickError.UserNotFound); // -5
if (order.IsPaid) return ClickPrepareResult.Fail(ClickError.AlreadyPaid); // -4
if (order.Amount != r.AmountValue) return ClickPrepareResult.Fail(ClickError.IncorrectAmount); // -2
var payment = new Payment
{
OrderId = order.Id,
ClickTransId = r.ClickTransId,
ClickPaydocId = r.ClickPaydocId,
Amount = r.AmountValue,
State = PaymentState.Preparing,
};
db.Payments.Add(payment);
await db.SaveChangesAsync(ct);
// Click stores this id and sends it back on Complete as merchant_prepare_id.
return ClickPrepareResult.Ok(payment.Id);
}
// STEP 2 — Click reports the charge result. Finalize.
public async Task<ClickCompleteResult> OnCompleteAsync(ClickShopRequest r, CancellationToken ct)
{
var payment = await db.Payments
.FirstOrDefaultAsync(p => p.Id == r.MerchantPrepareId, ct);
// The prepare record must exist and belong to this order.
if (payment is null || payment.OrderId != r.MerchantTransId)
return ClickCompleteResult.Fail(ClickError.TransactionNotFound); // -6
// Click failed/cancelled on its side → release and answer -9.
if (r.Error < 0)
{
payment.State = PaymentState.Cancelled;
await db.SaveChangesAsync(ct);
return ClickCompleteResult.Fail(ClickError.TransactionCancelled); // -9
}
// Idempotency: Click may retry Complete. Don't double-deliver.
if (payment.State == PaymentState.Paid)
return ClickCompleteResult.Ok(payment.Id);
if (payment.State == PaymentState.Cancelled)
return ClickCompleteResult.Fail(ClickError.TransactionCancelled);
// Atomic: mark paid + fulfill in one transaction.
await using var tx = await db.Database.BeginTransactionAsync(ct);
try
{
payment.State = PaymentState.Paid;
var order = await db.Orders.FirstAsync(o => o.Id == payment.OrderId, ct);
order.IsPaid = true;
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}
catch (Exception ex)
{
await tx.RollbackAsync(ct);
log.LogError(ex, "Click complete failed for order {Order}", r.MerchantTransId);
return ClickCompleteResult.Fail(ClickError.FailedToUpdate); // -7
}
log.LogInformation("Order {Order} paid via Click (paydoc {Doc})", r.MerchantTransId, r.ClickPaydocId);
return ClickCompleteResult.Ok(payment.Id);
}
}
The handler is registered scoped, so you can inject
DbContext, repositories, or any service. Not using EF Core? Swap it for Dapper, MongoDB, or anything — the SDK never touches your data.
r.Amountis the exact text Click signed;r.AmountValueis the same value parsed as adecimal. A non-numeric amount is rejected with-8before your handler runs, soAmountValuenever throws.
3. Send the user to checkout
public string Pay(ClickPaymentLink link) =>
link.Build(amount: 1000m, merchantTransId: "order-42", returnUrl: "https://shop.uz/done");
// => https://my.click.uz/services/pay?service_id=...&merchant_id=...&amount=1000.00&transaction_param=order-42&return_url=...
// optional 4th arg cardType: "uzcard" or "humo"
4. Merchant API (server-initiated)
Inject ClickMerchantClient anywhere:
public class Billing(ClickMerchantClient click)
{
public Task<InvoiceCreateResponse> Invoice() =>
click.CreateInvoiceAsync(1000m, "+998901234567", "order-42");
public Task<PaymentStatusResponse> Status() =>
click.GetPaymentStatusByMerchantTransIdAsync("order-42"); // optional 2nd arg: the DateOnly the payment was created (defaults to today UTC)
public Task<ClickApiResponse> Refund(long paymentId) =>
click.ReversePaymentAsync(paymentId);
// Card tokens: request -> verify (SMS) -> pay
public async Task PayByToken()
{
var t = await click.RequestCardTokenAsync("8600123456789012", "0399", temporary: false); // expire_date is MMYY
await click.VerifyCardTokenAsync(t.CardToken!, "12345");
await click.PayWithCardTokenAsync(t.CardToken!, 1000m, "order-42");
}
// CLICK Pass (QR/POS): charge the QR the customer shows
public Task<ClickPassPaymentResponse> Pass(string qrContent) =>
click.ClickPassPaymentAsync(qrContent, 5000m, cashboxCode: "KASSA-1");
// Fiscalization (OFD): submit receipt items after a payment
public Task<ClickApiResponse> Fiscalize(long paymentId) =>
click.SubmitFiscalItemsAsync(paymentId, new[]
{
new FiscalItem
{
Name = "Coffee", Spic = "12345678901234567", PackageCode = "1500001",
Price = 500000, Amount = 1, Vat = 53571, VatPercent = 12,
CommissionInfo = new FiscalCommissionInfo { Tin = "123456789" },
},
}, receivedCard: 500000);
// Any endpoint not wrapped yet:
public Task<ClickApiResponse> Raw() =>
click.CallAsync<ClickApiResponse>(HttpMethod.Get, "some/new/endpoint");
}
Other CLICK Pass calls: ConfirmClickPassAsync, EnableClickPassConfirmModeAsync, DisableClickPassConfirmModeAsync.
Other fiscalization calls: SubmitFiscalQrCodeAsync, GetFiscalDataAsync.
Merchant API calls never throw on a business or transport error — they return a response whose
IsSuccess is false. HTTP failures that carry no JSON body (a 401, a gateway 502) surface as
ErrorCode = -{statusCode}. Always check IsSuccess.
5. Logging
Every processor and the Merchant API client take an optional ILogger<T>; with ASP.NET Core DI it
is wired automatically, so Program.cs needs no extra code. What you get:
| Level | Logged |
|---|---|
Warning |
signature check failed, unknown service_id, malformed callback, any non-zero answer to Click, Merchant API errors |
Debug |
each callback dispatched, each Merchant API request (method + path) |
Request bodies are never logged — they carry card numbers and tokens. The secret key is never logged.
Turn the detail up in appsettings.json:
{ "Logging": { "LogLevel": { "Click.Sdk": "Debug" } } }
Outside DI, pass the logger yourself:
var processor = new ClickShopProcessor(options, handler, loggerFactory.CreateLogger<ClickShopProcessor>());
Without ASP.NET Core
ClickShopProcessor is framework-agnostic — feed it a field lookup, get back a response:
var processor = new ClickShopProcessor(options, handler);
var response = await processor.ProcessAsync(name => myForm[name]);
var json = ClickShopJson.Serialize(response);
Shop API error codes
0 success · -1 sign check failed · -2 incorrect amount · -3 action not found · -4 already paid · -5 user/order not found · -6 transaction not found · -7 failed to update · -8 bad request · -9 cancelled.
Front-end card payment (no redirect)
For paying by card in a popup over your site (no redirect), Click provides a JS library
https://my.click.uz/pay/checkout.js. That's a front-end concern — drop the <script> with
data-service-id, data-merchant-id, data-transaction-param, data-amount, data-card-type
into your payment form. The server side is the same Shop API callback this SDK already handles.
Advanced Shop API (billing services)
For utility/biller-style services Click uses a JSON flow (Getinfo → Prepare → Complete → Check → Compare)
with a dynamic params object and a different signature. The SDK handles parsing, the
md5(click_paydoc_id + attempt_trans_id + service_id + SECRET_KEY + paramsValues + action + sign_time)
verification (on Prepare/Complete/Check), and the response shape.
builder.Services.AddClick(builder.Configuration)
.AddClickAdvancedShop<BillingHandler>();
app.MapClickAdvancedShopApi(); // POST /click/advanced-shop
public sealed class BillingHandler : IClickAdvancedShopHandler
{
public Task<AdvancedGetInfoResult> OnGetInfoAsync(AdvancedShopRequest r, CancellationToken ct)
{
var contract = r.Param("contract");
return Task.FromResult(AdvancedGetInfoResult.Ok(new Dictionary<string,string>
{
["fio"] = "Ivan Ivanov", ["address"] = "Tashkent",
}));
}
public Task<AdvancedPrepareResult> OnPrepareAsync(AdvancedShopRequest r, CancellationToken ct)
=> Task.FromResult(AdvancedPrepareResult.Ok(merchantPrepareId: 12345));
public Task<AdvancedCompleteResult> OnCompleteAsync(AdvancedShopRequest r, CancellationToken ct)
=> Task.FromResult(AdvancedCompleteResult.Ok(merchantConfirmId: 12345));
public Task<AdvancedCheckResult> OnCheckAsync(AdvancedShopRequest r, CancellationToken ct)
=> Task.FromResult(AdvancedCheckResult.Ok(merchantConfirmId: 12345, status: 2));
public Task<AdvancedCompareResult> OnCompareAsync(AdvancedShopRequest r, CancellationToken ct)
=> Task.FromResult(AdvancedCompareResult.Ok(new { /* requests in [from_date, till_date] */ }));
}
Multi-tenant
Serving several organizations from one app? Implement IClickOptionsResolver to look credentials
up by service_id and register it instead of the single-tenant AddClick:
builder.Services.AddClickMultiTenant<DbClickOptionsResolver>()
.AddClickShopHandler<OrderClickHandler>();
Inbound callbacks then pick the right secret automatically; a signature made with tenant A's key is
rejected for tenant B. For outbound calls inject ClickMerchantClientFactory / ClickPaymentLinkFactory
and call CreateAsync(serviceId).
Notes
SecretKeysigns both Shop API callbacks (md5) and Merchant API requests (sha1 digest). Keep it in secrets, not source.- Verified against the official Click docs (docs.click.uz): Shop API, Merchant API, payment button, CLICK Pass and fiscalization.
- 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 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
- BakhodirovDev.Click.Sdk (>= 0.1.0)
-
net8.0
- BakhodirovDev.Click.Sdk (>= 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 | 88 | 8/6/2026 |