Payme.Merchant
1.0.0
dotnet add package Payme.Merchant --version 1.0.0
NuGet\Install-Package Payme.Merchant -Version 1.0.0
<PackageReference Include="Payme.Merchant" Version="1.0.0" />
<PackageVersion Include="Payme.Merchant" Version="1.0.0" />
<PackageReference Include="Payme.Merchant" />
paket add Payme.Merchant --version 1.0.0
#r "nuget: Payme.Merchant, 1.0.0"
#:package Payme.Merchant@1.0.0
#addin nuget:?package=Payme.Merchant&version=1.0.0
#tool nuget:?package=Payme.Merchant&version=1.0.0
Payme Merchant API for .NET
Unofficial .NET Standard 2.1 library for integrating Payme (Paycom Uzbekistan) Merchant API.
π Navigation
Overview
This library provides a complete implementation of the Payme Merchant API for .NET applications. It handles all the complexity of JSON-RPC 2.0 communication, transaction state management, and security validation, allowing you to focus on your business logic.
Features
- β Full JSON-RPC 2.0 Compliance β Strict adherence to the specification with proper error handling
- π Built-in Security β Basic Authentication validation out of the box
- π Multilingual Error Messages β Supports Russian, Uzbek, and English error responses
- π Idempotency Support β Proper handling of duplicate transaction requests
- βοΈ Dependency Injection β ASP.NET Core DI integration via extension methods
- π¦ Clean Architecture β Separation of concerns with interfaces for business logic
Installation
Option 1: Add Project Reference
Add the Payme.Merchant project to your solution and reference it.
Option 2: NuGet Package (Coming Soon)
dotnet add package Payme.Merchant
Quick Start
1. Register Services
In your Program.cs (ASP.NET Core 6+):
using Payme.Merchant.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Register Payme Merchant with your credentials
builder.Services.AddPaymeMerchant(options =>
{
options.Login = "Paycom"; // Default login
options.Password = "YOUR_SECRET_KEY"; // From Payme Merchant Cabinet
});
// Register your implementations of the required interfaces
builder.Services.AddScoped<IPaymeOrderService, YourOrderService>();
builder.Services.AddScoped<IPaymeTransactionStore, YourTransactionStore>();
2. Implement Required Interfaces
You must implement two interfaces to connect the library to your business logic:
IPaymeOrderService
Handles order validation and status management:
public interface IPaymeOrderService
{
/// <summary>
/// Checks if an order with the given ID exists.
/// </summary>
Task<bool> ExistsAsync(string orderId);
/// <summary>
/// Checks if the order has already been paid.
/// </summary>
Task<bool> IsPaidAsync(string orderId);
/// <summary>
/// Gets the order amount in tiyin (1 UZS = 100 tiyin).
/// </summary>
Task<long> GetAmountAsync(string orderId);
/// <summary>
/// Marks the order as paid after successful transaction.
/// </summary>
Task MarkAsPaidAsync(string orderId);
/// <summary>
/// Cancels/refunds the order.
/// </summary>
/// <param name="orderId">The order ID</param>
/// <param name="reason">Cancellation reason code from Payme</param>
Task CancelAsync(string orderId, int reason);
}
IPaymeTransactionStore
Handles transaction persistence (critical for idempotency):
public interface IPaymeTransactionStore
{
/// <summary>
/// Creates a new transaction record.
/// </summary>
/// <param name="paymeTransactionId">Unique ID from Payme</param>
/// <param name="orderId">Your order ID</param>
/// <param name="time">Transaction creation time (Unix ms)</param>
/// <param name="amount">Amount in tiyin</param>
Task CreateAsync(string paymeTransactionId, string orderId, long time, long amount);
/// <summary>
/// Retrieves a transaction by its Payme ID.
/// </summary>
Task<PaymeTransaction?> GetAsync(string paymeTransactionId);
/// <summary>
/// Retrieves a transaction by order ID.
/// </summary>
/// <remarks>
/// Used for idempotency checks when multiple transactions target the same order.
/// </remarks>
Task<PaymeTransaction?> GetByOrderIdAsync(string orderId);
/// <summary>
/// Marks a transaction as performed/completed.
/// </summary>
Task PerformAsync(string paymeTransactionId, long performTime);
/// <summary>
/// Cancels a transaction.
/// </summary>
/// <param name="paymeTransactionId">The Payme transaction ID</param>
/// <param name="cancelTime">Cancellation timestamp (Unix ms)</param>
/// <param name="reason">Cancellation reason code</param>
/// <param name="newState">New state (-1 for Cancelled, -2 for CancelledAfterComplete)</param>
Task CancelAsync(string paymeTransactionId, long cancelTime, int reason, int newState);
}
3. Create Controller Endpoint
Create an API endpoint to receive Payme callbacks:
[Route("api/[controller]")]
[ApiController]
public class PaymeController : ControllerBase
{
private readonly PaymeDispatcher _dispatcher;
public PaymeController(PaymeDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
[HttpPost]
public async Task<IActionResult> Post()
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
var body = await reader.ReadToEndAsync();
var authHeader = Request.Headers["Authorization"].ToString();
// Dispatches request and returns JSON-RPC response
var responseJson = await _dispatcher.DispatchAsync(authHeader, body);
return Content(responseJson, "application/json");
}
}
API Reference
Configuration Options
| Property | Type | Default | Description |
|---|---|---|---|
Login |
string |
"Paycom" |
Payme merchant login |
Password |
string |
- | Secret key from Payme Merchant Cabinet |
Transaction States
The library uses strict state management as per Payme specification:
| State | Value | Description |
|---|---|---|
Created |
1 |
Transaction created, awaiting payment confirmation |
Completed |
2 |
Payment successfully completed |
Cancelled |
-1 |
Transaction cancelled before completion |
CancelledAfterComplete |
-2 |
Transaction refunded after completion |
public enum TransactionState
{
Created = 1,
Completed = 2,
Cancelled = -1,
CancelledAfterComplete = -2
}
PaymeTransaction Model
The transaction model used by IPaymeTransactionStore:
public class PaymeTransaction
{
public string Id { get; set; } // Payme transaction ID
public string OrderId { get; set; } // Your order ID
public long Time { get; set; } // Request time (Unix ms)
public long Amount { get; set; } // Amount in tiyin
public int State { get; set; } // TransactionState
public long CreateTime { get; set; } // Creation timestamp (Unix ms)
public long PerformTime { get; set; } // Completion timestamp (Unix ms)
public long CancelTime { get; set; } // Cancellation timestamp (Unix ms)
public int Reason { get; set; } // Cancellation reason code
}
Supported Methods
The PaymeDispatcher handles all standard Payme Merchant API methods:
| Method | Description |
|---|---|
CheckPerformTransaction |
Validates order before payment |
CreateTransaction |
Creates a new payment transaction |
PerformTransaction |
Confirms and completes payment |
CancelTransaction |
Cancels/refunds a transaction |
CheckTransaction |
Checks current transaction status |
GetStatement |
Returns transaction history (not fully implemented) |
ChangePassword |
Updates merchant password |
Error Handling
Error Codes
The library provides predefined error codes matching Payme specification:
| Code | Constant | Description |
|---|---|---|
-32700 |
InvalidJson |
Parse error |
-32601 |
MethodNotFound |
Method not found |
-32504 |
AccessDenied |
Authentication failed |
-32400 |
SystemError |
Internal server error |
-32300 |
TransportError |
Transport level error |
-31001 |
WrongAmount |
Incorrect amount |
-31003 |
TransactionNotFound |
Transaction not found |
-31007 |
CantCancel |
Cannot cancel transaction |
-31008 |
UnableToComplete |
Cannot complete operation |
-31050 |
OrderNotFound |
Order not found |
-31088 |
InProgress |
Transaction in progress |
-31098 |
TerminalState |
Transaction already completed |
-31099 |
Pending |
Temporary error, try later |
Multilingual Error Messages
All error messages are returned in three languages:
public static readonly PaymeErrorMessage OrderNotFound = new PaymeErrorMessage(
ru: "ΠΠΎΠΌΠ΅Ρ ΡΠ΅Π»Π΅ΡΠΎΠ½Π° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½",
uz: "Raqam ro'yhatda yo'q",
en: "Phone number not found"
);
Throwing Custom Errors
Use PaymeException to throw errors with proper JSON-RPC formatting:
// With predefined multilingual message
throw new PaymeException(
PaymeErrorCodes.OrderNotFound,
PaymeErrors.OrderNotFound,
"order_id" // Optional data field
);
// With simple string message
throw new PaymeException(
PaymeErrorCodes.WrongAmount,
"The amount does not match"
);
Payment Flow
Here's how a typical payment flow works:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PAYMENT FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. User clicks "Pay with Payme" on your site
β
βΌ
2. Redirect to: https://checkout.paycom.uz/{ENCODED_PARAMS}
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PAYME SENDS CALLBACKS TO YOUR API β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββΊ CheckPerformTransaction
β β’ Validates order exists
β β’ Verifies amount matches
β β’ Checks order not already paid
β β’ Returns { allow: true } if valid
β
ββββΊ CreateTransaction
β β’ Creates transaction in state=1 (Created)
β β’ Reserves money (not yet transferred)
β β’ Handles idempotency (returns existing if same ID)
β
ββββΊ PerformTransaction
β β’ Moves to state=2 (Completed)
β β’ Money transferred to merchant
β β’ Calls MarkAsPaidAsync on your order
β
ββββΊ CancelTransaction (if needed)
β’ Moves to state=-1 (Cancelled) or -2 (CancelledAfterComplete)
β’ Initiates refund
β’ Calls CancelAsync on your order
Transaction Timeout
Transactions in Created state have a 12-hour timeout. If PerformTransaction is not called within this window, the library automatically cancels the transaction.
Examples
In-Memory Implementation (for Testing)
See the complete example in Payme.Merchant.Example/Services/InMemoryPaymeService.cs:
public class InMemoryPaymeService : IPaymeOrderService, IPaymeTransactionStore
{
private readonly ConcurrentDictionary<string, Order> _orders = new();
private readonly ConcurrentDictionary<string, PaymeTransaction> _transactions = new();
public InMemoryPaymeService()
{
// Seed test orders
_orders.TryAdd("1001", new Order { Id = "1001", Amount = 100000, IsPaid = false });
_orders.TryAdd("1002", new Order { Id = "1002", Amount = 500000, IsPaid = false });
}
// IPaymeOrderService implementation
public Task<bool> ExistsAsync(string orderId)
=> Task.FromResult(_orders.ContainsKey(orderId));
public Task<long> GetAmountAsync(string orderId)
{
if (_orders.TryGetValue(orderId, out var order))
return Task.FromResult(order.Amount);
return Task.FromResult(0L);
}
public Task<bool> IsPaidAsync(string orderId)
{
if (_orders.TryGetValue(orderId, out var order))
return Task.FromResult(order.IsPaid);
return Task.FromResult(false);
}
public Task MarkAsPaidAsync(string orderId)
{
if (_orders.TryGetValue(orderId, out var order))
order.IsPaid = true;
return Task.CompletedTask;
}
public Task CancelAsync(string orderId, int reason)
{
if (_orders.TryGetValue(orderId, out var order))
order.IsPaid = false;
return Task.CompletedTask;
}
// IPaymeTransactionStore implementation
public Task CreateAsync(string paymeTransactionId, string orderId, long time, long amount)
{
var transaction = new PaymeTransaction
{
Id = paymeTransactionId,
OrderId = orderId,
Time = time,
Amount = amount,
CreateTime = time,
State = (int)TransactionState.Created
};
_transactions.TryAdd(paymeTransactionId, transaction);
return Task.CompletedTask;
}
public Task<PaymeTransaction?> GetAsync(string paymeTransactionId)
{
_transactions.TryGetValue(paymeTransactionId, out var t);
return Task.FromResult(t);
}
public Task<PaymeTransaction?> GetByOrderIdAsync(string orderId)
{
var t = _transactions.Values.FirstOrDefault(x => x.OrderId == orderId);
return Task.FromResult(t);
}
public Task PerformAsync(string paymeTransactionId, long performTime)
{
if (_transactions.TryGetValue(paymeTransactionId, out var t))
{
t.State = (int)TransactionState.Completed;
t.PerformTime = performTime;
}
return Task.CompletedTask;
}
public Task CancelAsync(string paymeTransactionId, long cancelTime, int reason, int newState)
{
if (_transactions.TryGetValue(paymeTransactionId, out var t))
{
t.State = newState;
t.CancelTime = cancelTime;
t.Reason = reason;
}
return Task.CompletedTask;
}
}
Entity Framework Implementation
public class EfPaymeTransactionStore : IPaymeTransactionStore
{
private readonly AppDbContext _context;
public EfPaymeTransactionStore(AppDbContext context)
{
_context = context;
}
public async Task CreateAsync(string paymeTransactionId, string orderId, long time, long amount)
{
var transaction = new PaymeTransactionEntity
{
Id = paymeTransactionId,
OrderId = orderId,
Time = time,
Amount = amount,
CreateTime = time,
State = (int)TransactionState.Created
};
_context.PaymeTransactions.Add(transaction);
await _context.SaveChangesAsync();
}
public async Task<PaymeTransaction?> GetAsync(string paymeTransactionId)
{
var entity = await _context.PaymeTransactions
.FirstOrDefaultAsync(t => t.Id == paymeTransactionId);
return entity == null ? null : MapToPaymeTransaction(entity);
}
public async Task<PaymeTransaction?> GetByOrderIdAsync(string orderId)
{
var entity = await _context.PaymeTransactions
.FirstOrDefaultAsync(t => t.OrderId == orderId);
return entity == null ? null : MapToPaymeTransaction(entity);
}
public async Task PerformAsync(string paymeTransactionId, long performTime)
{
var entity = await _context.PaymeTransactions
.FirstOrDefaultAsync(t => t.Id == paymeTransactionId);
if (entity != null)
{
entity.State = (int)TransactionState.Completed;
entity.PerformTime = performTime;
await _context.SaveChangesAsync();
}
}
public async Task CancelAsync(string paymeTransactionId, long cancelTime, int reason, int newState)
{
var entity = await _context.PaymeTransactions
.FirstOrDefaultAsync(t => t.Id == paymeTransactionId);
if (entity != null)
{
entity.State = newState;
entity.CancelTime = cancelTime;
entity.Reason = reason;
await _context.SaveChangesAsync();
}
}
private static PaymeTransaction MapToPaymeTransaction(PaymeTransactionEntity entity)
=> new()
{
Id = entity.Id,
OrderId = entity.OrderId,
Time = entity.Time,
Amount = entity.Amount,
State = entity.State,
CreateTime = entity.CreateTime,
PerformTime = entity.PerformTime,
CancelTime = entity.CancelTime,
Reason = entity.Reason
};
}
Security Notes
HTTPS is Required β Payme will only send callbacks to HTTPS endpoints. Ensure your endpoint has a valid SSL certificate.
Store Your Secret Key Securely β Never commit your Payme secret key to version control. Use environment variables or a secrets manager.
Basic Authentication β The library automatically validates the Authorization header against your configured credentials. Invalid credentials result in a -32504 Access Denied error.
Best Practices
Use environment variables for the secret key:
builder.Services.AddPaymeMerchant(options => { options.Password = builder.Configuration["Payme:SecretKey"]; });Implement proper logging β The
PaymeDispatcheraccepts an optionalILogger<PaymeDispatcher>for logging errors.Handle idempotency correctly β Always return the same response for duplicate
CreateTransactionrequests with the same ID.Test with Payme sandbox before going to production.
Project Structure
Payme.Merchant/
βββ Abstractions/
β βββ PaymeInterfaces.cs # IPaymeOrderService, IPaymeTransactionStore, PaymeTransaction
βββ Configuration/
β βββ PaymeOptions.cs # Configuration options
βββ Extensions/
β βββ ServiceCollectionExtensions.cs # DI registration
βββ Models/
β βββ JsonRpc/
β β βββ JsonRpcRequest.cs # JSON-RPC request model
β β βββ JsonRpcResponse.cs # JSON-RPC response model
β β βββ JsonRpcError.cs # JSON-RPC error model
β βββ MethodDtos/
β β βββ MethodDtos.cs # Request DTOs for each method
β β βββ MethodResults.cs # Response DTOs for each method
β βββ PaymeAccount.cs # Order account info (order_id)
β βββ PaymeErrorCodes.cs # Error code constants
β βββ PaymeErrorMessage.cs # Multilingual error message
β βββ PaymeErrors.cs # Predefined error messages
β βββ PaymeException.cs # Custom exception for Payme errors
β βββ TransactionState.cs # Transaction state enum
βββ PaymeDispatcher.cs # Main request dispatcher
Payme.Merchant.Example/
βββ Controllers/
β βββ PaymeController.cs # Example API endpoint
βββ Services/
β βββ InMemoryPaymeService.cs # Example in-memory implementation
βββ Program.cs # Example app configuration
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License.
Disclaimer
This is an unofficial library and is not affiliated with Paycom/Payme. Use at your own risk. Always test thoroughly in a sandbox environment before deploying to production.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Options (>= 10.0.1)
- Newtonsoft.Json (>= 13.0.4)
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 |
|---|---|---|
| 1.0.0 | 177 | 1/12/2026 |