BakhodirovDev.Click.Sdk 0.1.0

dotnet add package BakhodirovDev.Click.Sdk --version 0.1.0
                    
NuGet\Install-Package BakhodirovDev.Click.Sdk -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.Sdk" 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.Sdk" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="BakhodirovDev.Click.Sdk" />
                    
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.Sdk --version 0.1.0
                    
#r "nuget: BakhodirovDev.Click.Sdk, 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.Sdk@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.Sdk&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=BakhodirovDev.Click.Sdk&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 (1)

Showing the top 1 NuGet packages that depend on BakhodirovDev.Click.Sdk:

Package Downloads
BakhodirovDev.Click.AspNetCore

ASP.NET Core integration for the Click SDK: AddClick() DI and MapClickShopApi() endpoint mapping.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 96 8/6/2026