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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BakhodirovDev.Click.AspNetCore" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BakhodirovDev.Click.AspNetCore" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="BakhodirovDev.Click.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BakhodirovDev.Click.AspNetCore --version 0.1.0
                    
#r "nuget: BakhodirovDev.Click.AspNetCore, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BakhodirovDev.Click.AspNetCore@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BakhodirovDev.Click.AspNetCore&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=BakhodirovDev.Click.AspNetCore&version=0.1.0
                    
Install as a Cake Tool

Click .NET SDK

English · O'zbekcha · Русский

.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.Amount is the exact text Click signed; r.AmountValue is the same value parsed as a decimal. A non-numeric amount is rejected with -8 before your handler runs, so AmountValue never 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

  • SecretKey signs 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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